mirror of
https://github.com/barry-ran/QtScrcpy.git
synced 2025-04-24 13:34:59 +00:00
feat: sync scrcpy server
This commit is contained in:
parent
2c2ad2ab07
commit
a26620fb8d
40 changed files with 818 additions and 456 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -9,5 +9,5 @@
|
|||
/server/gradlew
|
||||
/server/gradlew.bat
|
||||
/server/local.properties
|
||||
build
|
||||
build-*
|
||||
*.DS_Store
|
||||
|
|
|
@ -58,8 +58,8 @@ INCLUDEPATH += \
|
|||
|
||||
# 统一版本号入口,只修改这一个地方即可
|
||||
VERSION_MAJOR = 1
|
||||
VERSION_MINOR = 0
|
||||
VERSION_PATCH = 4
|
||||
VERSION_MINOR = 12
|
||||
VERSION_PATCH = 1
|
||||
|
||||
# qmake变量的方式定义版本号
|
||||
VERSION = $${VERSION_MAJOR}.$${VERSION_MINOR}.$${VERSION_PATCH}
|
||||
|
|
|
@ -46,8 +46,8 @@ void Controller::postControlMsg(ControlMsg *controlMsg)
|
|||
|
||||
void Controller::test(QRect rc)
|
||||
{
|
||||
ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_MOUSE);
|
||||
controlMsg->setInjectMouseMsgData(AMOTION_EVENT_ACTION_DOWN, AMOTION_EVENT_BUTTON_PRIMARY, rc);
|
||||
ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_TOUCH);
|
||||
controlMsg->setInjectTouchMsgData(POINTER_ID_MOUSE, AMOTION_EVENT_ACTION_DOWN, AMOTION_EVENT_BUTTON_PRIMARY, rc, 1.0f);
|
||||
postControlMsg(controlMsg);
|
||||
}
|
||||
|
||||
|
|
|
@ -42,18 +42,13 @@ void ControlMsg::setInjectTextMsgData(QString& text)
|
|||
m_data.injectText.text[tmp.length()] = '\0';
|
||||
}
|
||||
|
||||
void ControlMsg::setInjectMouseMsgData(AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position)
|
||||
void ControlMsg::setInjectTouchMsgData(quint64 id, AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position, float pressure)
|
||||
{
|
||||
m_data.injectMouse.action = action;
|
||||
m_data.injectMouse.buttons = buttons;
|
||||
m_data.injectMouse.position = position;
|
||||
}
|
||||
|
||||
void ControlMsg::setInjectTouchMsgData(quint32 id, AndroidMotioneventAction action, QRect position)
|
||||
{
|
||||
m_data.injectTouch.action = action;
|
||||
m_data.injectTouch.id = id;
|
||||
m_data.injectTouch.action = action;
|
||||
m_data.injectTouch.buttons = buttons;
|
||||
m_data.injectTouch.position = position;
|
||||
m_data.injectTouch.pressure = pressure;
|
||||
}
|
||||
|
||||
void ControlMsg::setInjectScrollMsgData(QRect position, qint32 hScroll, qint32 vScroll)
|
||||
|
@ -85,12 +80,22 @@ void ControlMsg::setSetScreenPowerModeData(ControlMsg::ScreenPowerMode mode)
|
|||
|
||||
void ControlMsg::writePosition(QBuffer &buffer, const QRect& value)
|
||||
{
|
||||
BufferUtil::write16(buffer, value.left());
|
||||
BufferUtil::write16(buffer, value.top());
|
||||
BufferUtil::write32(buffer, value.left());
|
||||
BufferUtil::write32(buffer, value.top());
|
||||
BufferUtil::write16(buffer, value.width());
|
||||
BufferUtil::write16(buffer, value.height());
|
||||
}
|
||||
|
||||
quint16 ControlMsg::toFixedPoint16(float f)
|
||||
{
|
||||
assert(f >= 0.0f && f <= 1.0f);
|
||||
quint32 u = f * 0x1p16f; // 2^16
|
||||
if (u >= 0xffff) {
|
||||
u = 0xffff;
|
||||
}
|
||||
return (quint16) u;
|
||||
}
|
||||
|
||||
QByteArray ControlMsg::serializeData()
|
||||
{
|
||||
QByteArray byteArray;
|
||||
|
@ -108,15 +113,15 @@ QByteArray ControlMsg::serializeData()
|
|||
BufferUtil::write16(buffer, strlen(m_data.injectText.text));
|
||||
buffer.write(m_data.injectText.text, strlen(m_data.injectText.text));
|
||||
break;
|
||||
case CMT_INJECT_MOUSE:
|
||||
buffer.putChar(m_data.injectMouse.action);
|
||||
BufferUtil::write32(buffer, m_data.injectMouse.buttons);
|
||||
writePosition(buffer, m_data.injectMouse.position);
|
||||
break;
|
||||
case CMT_INJECT_TOUCH:
|
||||
buffer.putChar(m_data.injectTouch.id);
|
||||
{
|
||||
buffer.putChar(m_data.injectTouch.action);
|
||||
BufferUtil::write64(buffer, m_data.injectTouch.id);
|
||||
writePosition(buffer, m_data.injectTouch.position);
|
||||
quint16 pressure = toFixedPoint16(m_data.injectTouch.pressure);
|
||||
BufferUtil::write16(buffer, pressure);
|
||||
BufferUtil::write32(buffer, m_data.injectTouch.buttons);
|
||||
}
|
||||
break;
|
||||
case CMT_INJECT_SCROLL:
|
||||
writePosition(buffer, m_data.injectScroll.position);
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
#define CONTROL_MSG_TEXT_MAX_LENGTH 300
|
||||
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093
|
||||
#define POINTER_ID_MOUSE UINT64_C(-1)
|
||||
// ControlMsg
|
||||
class ControlMsg : public QScrcpyEvent
|
||||
{
|
||||
|
@ -19,16 +20,14 @@ public:
|
|||
CMT_NULL = -1,
|
||||
CMT_INJECT_KEYCODE = 0,
|
||||
CMT_INJECT_TEXT,
|
||||
CMT_INJECT_MOUSE,
|
||||
CMT_INJECT_TOUCH,
|
||||
CMT_INJECT_SCROLL,
|
||||
CMT_BACK_OR_SCREEN_ON,
|
||||
CMT_EXPAND_NOTIFICATION_PANEL,
|
||||
CMT_COLLAPSE_NOTIFICATION_PANEL,
|
||||
CMT_GET_CLIPBOARD,
|
||||
CMT_SET_CLIPBOARD,
|
||||
CMT_SET_SCREEN_POWER_MODE,
|
||||
|
||||
CMT_INJECT_TOUCH,
|
||||
CMT_SET_SCREEN_POWER_MODE
|
||||
};
|
||||
|
||||
enum ScreenPowerMode {
|
||||
|
@ -42,11 +41,10 @@ public:
|
|||
|
||||
void setInjectKeycodeMsgData(AndroidKeyeventAction action, AndroidKeycode keycode, AndroidMetastate metastate);
|
||||
void setInjectTextMsgData(QString& text);
|
||||
void setInjectMouseMsgData(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 setInjectTouchMsgData(quint32 id, AndroidMotioneventAction action, QRect position);
|
||||
void setInjectTouchMsgData(quint64 id, AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position, float pressure);
|
||||
void setInjectScrollMsgData(QRect position, qint32 hScroll, qint32 vScroll);
|
||||
void setSetClipboardMsgData(QString& text);
|
||||
void setSetScreenPowerModeData(ControlMsg::ScreenPowerMode mode);
|
||||
|
@ -55,6 +53,7 @@ public:
|
|||
|
||||
private:
|
||||
void writePosition(QBuffer& buffer, const QRect& value);
|
||||
quint16 toFixedPoint16(float f);
|
||||
|
||||
private:
|
||||
struct ControlMsgData {
|
||||
|
@ -69,14 +68,11 @@ private:
|
|||
char* text = Q_NULLPTR;
|
||||
} injectText;
|
||||
struct {
|
||||
quint64 id;
|
||||
AndroidMotioneventAction action;
|
||||
AndroidMotioneventButtons buttons;
|
||||
QRect position;
|
||||
} injectMouse;
|
||||
struct {
|
||||
quint32 id;
|
||||
AndroidMotioneventAction action;
|
||||
QRect position;
|
||||
float pressure;
|
||||
} injectTouch;
|
||||
struct {
|
||||
QRect position;
|
||||
|
|
|
@ -137,7 +137,7 @@ void InputConvertGame::sendTouchEvent(int id, QPointF pos, AndroidMotioneventAct
|
|||
if (!controlMsg) {
|
||||
return;
|
||||
}
|
||||
controlMsg->setInjectTouchMsgData(id, action, QRect(calcFrameAbsolutePos(pos).toPoint(), m_frameSize));
|
||||
controlMsg->setInjectTouchMsgData(id, action, (AndroidMotioneventButtons)0, QRect(calcFrameAbsolutePos(pos).toPoint(), m_frameSize), 1.0f);
|
||||
sendControlMsg(controlMsg);
|
||||
}
|
||||
|
||||
|
|
|
@ -40,11 +40,11 @@ void InputConvertNormal::mouseEvent(const QMouseEvent* from, const QSize& frameS
|
|||
pos.setY(pos.y() * frameSize.height() / showSize.height());
|
||||
|
||||
// set data
|
||||
ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_MOUSE);
|
||||
ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_TOUCH);
|
||||
if (!controlMsg) {
|
||||
return;
|
||||
}
|
||||
controlMsg->setInjectMouseMsgData(action, convertMouseButtons(from->buttons()), QRect(pos.toPoint(), frameSize));
|
||||
controlMsg->setInjectTouchMsgData(POINTER_ID_MOUSE, action, convertMouseButtons(from->buttons()), QRect(pos.toPoint(), frameSize), 1.0f);
|
||||
sendControlMsg(controlMsg);
|
||||
}
|
||||
|
||||
|
|
|
@ -203,6 +203,7 @@ void Device::startServer()
|
|||
params.localPort = m_params.localPort;
|
||||
params.maxSize = m_params.maxSize;
|
||||
params.bitRate = m_params.bitRate;
|
||||
params.maxFps = m_params.maxFps;
|
||||
params.crop = "-";
|
||||
params.sendFrameMeta = m_recorder ? true : false;
|
||||
params.control = true;
|
||||
|
|
|
@ -22,6 +22,7 @@ public:
|
|||
quint16 localPort = 27183; // reverse时本地监听端口
|
||||
quint16 maxSize = 720; // 视频分辨率
|
||||
quint32 bitRate = 8000000; // 视频比特率
|
||||
quint32 maxFps = 60; // 视频最大帧率
|
||||
bool closeScreen = false; // 启动时自动息屏
|
||||
bool useReverse = true; // true:先使用adb reverse,失败后自动使用adb forward;false:直接使用adb forward
|
||||
bool display = true; // 是否显示画面(或者仅仅后台录制)
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
|
||||
#define DEVICE_NAME_FIELD_LENGTH 64
|
||||
#define SOCKET_NAME "qtscrcpy"
|
||||
#define SOCKET_NAME "scrcpy"
|
||||
#define MAX_CONNECT_COUNT 30
|
||||
#define MAX_RESTART_COUNT 1
|
||||
|
||||
|
@ -129,8 +129,13 @@ bool Server::execute()
|
|||
args << "app_process";
|
||||
args << "/"; // unused;
|
||||
args << "com.genymobile.scrcpy.Server";
|
||||
// version
|
||||
QStringList versionList = QCoreApplication::applicationVersion().split(".");
|
||||
QString version = versionList[0] + "." + versionList[1] + "." + versionList[2];
|
||||
args << version;
|
||||
args << QString::number(m_params.maxSize);
|
||||
args << QString::number(m_params.bitRate);
|
||||
args << QString::number(m_params.maxFps);
|
||||
args << (m_tunnelForward ? "true" : "false");
|
||||
if (m_params.crop.isEmpty()) {
|
||||
args << "-";
|
||||
|
|
|
@ -27,6 +27,7 @@ public:
|
|||
quint16 localPort = 27183; // reverse时本地监听端口
|
||||
quint16 maxSize = 720; // 视频分辨率
|
||||
quint32 bitRate = 8000000; // 视频比特率
|
||||
quint32 maxFps = 60; // 视频最大帧率
|
||||
QString crop = "-"; // 视频裁剪
|
||||
bool sendFrameMeta = false; // 是否发送mp4帧数据
|
||||
bool control = true; // 安卓端是否接收键鼠控制
|
||||
|
|
|
@ -149,6 +149,8 @@ void Dialog::on_startServerBtn_clicked()
|
|||
params.serial = ui->serialBox->currentText().trimmed();
|
||||
params.maxSize = videoSize;
|
||||
params.bitRate = bitRate;
|
||||
// on devices with Android >= 10, the capture frame rate can be limited
|
||||
params.maxFps = 60;
|
||||
params.recordFileName = absFilePath;
|
||||
params.closeScreen = ui->closeScreenCheck->isChecked();
|
||||
params.useReverse = ui->useReverseCheck->isChecked();
|
||||
|
|
|
@ -8,6 +8,12 @@ void BufferUtil::write32(QBuffer &buffer, quint32 value)
|
|||
buffer.putChar(value);
|
||||
}
|
||||
|
||||
void BufferUtil::write64(QBuffer &buffer, quint64 value)
|
||||
{
|
||||
write32(buffer, value >> 32);
|
||||
write32(buffer, (quint32) value);
|
||||
}
|
||||
|
||||
void BufferUtil::write16(QBuffer &buffer, quint32 value)
|
||||
{
|
||||
buffer.putChar(value >> 8);
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
|
||||
class BufferUtil
|
||||
{
|
||||
public:
|
||||
static void write32(QBuffer& buffer, quint32 value);
|
||||
public:
|
||||
static void write16(QBuffer& buffer, quint32 value);
|
||||
static void write32(QBuffer& buffer, quint32 value);
|
||||
static void write64(QBuffer& buffer, quint64 value);
|
||||
static quint16 read16(QBuffer& buffer);
|
||||
static quint32 read32(QBuffer& buffer);
|
||||
static quint64 read64(QBuffer& buffer);
|
||||
|
|
|
@ -185,7 +185,7 @@ Mac OS平台,你可以直接使用我编译好的可执行程序:
|
|||
### Android端 (没有修改需求的话直接使用自带的scrcpy-server.jar即可)
|
||||
1. 目标平台上搭建Android开发环境
|
||||
2. 使用Android Studio打开项目根目录中的server项目
|
||||
3. 第一次打开如果你没有对应版本的gradle会提示找不到gradle,是否升级gradle并创建,选择取消,取消后会弹出gradle选择已有gradle的位置,同样取消即可(会自动下载)
|
||||
3. 第一次打开如果你没有对应版本的gradle会提示找不到gradle,是否升级gradle并创建,选择取消,取消后会弹出选择已有gradle的位置,同样取消即可(会自动下载)
|
||||
4. 按需编辑代码即可,当然也可以不编辑
|
||||
4. 编译出apk以后改名为scrcpy-server.jar并替换third_party/scrcpy-server.jar即可
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ android {
|
|||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 5
|
||||
versionName "1.4"
|
||||
versionName "1.12.1"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
/**
|
||||
* Union of all supported msg types, identified by their {@code type}.
|
||||
* Union of all supported event types, identified by their {@code type}.
|
||||
*/
|
||||
public final class ControlMessage {
|
||||
|
||||
public static final int TYPE_INJECT_KEYCODE = 0;
|
||||
public static final int TYPE_INJECT_TEXT = 1;
|
||||
public static final int TYPE_INJECT_MOUSE = 2;
|
||||
public static final int TYPE_INJECT_SCROLL = 3;
|
||||
public static final int TYPE_INJECT_TOUCH_EVENT = 2;
|
||||
public static final int TYPE_INJECT_SCROLL_EVENT = 3;
|
||||
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_SET_SCREEN_POWER_MODE = 9;
|
||||
|
||||
public static final int TYPE_INJECT_TOUCH = 10;
|
||||
|
||||
public static final int TYPE_ROTATE_DEVICE = 10;
|
||||
|
||||
private int type;
|
||||
private String text;
|
||||
|
@ -25,7 +23,8 @@ public final class ControlMessage {
|
|||
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
|
||||
private int keycode; // KeyEvent.KEYCODE_*
|
||||
private int buttons; // MotionEvent.BUTTON_*
|
||||
private int id;
|
||||
private long pointerId;
|
||||
private float pressure;
|
||||
private Position position;
|
||||
private int hScroll;
|
||||
private int vScroll;
|
||||
|
@ -34,69 +33,62 @@ public final class ControlMessage {
|
|||
}
|
||||
|
||||
public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) {
|
||||
ControlMessage event = new ControlMessage();
|
||||
event.type = TYPE_INJECT_KEYCODE;
|
||||
event.action = action;
|
||||
event.keycode = keycode;
|
||||
event.metaState = metaState;
|
||||
return event;
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_INJECT_KEYCODE;
|
||||
msg.action = action;
|
||||
msg.keycode = keycode;
|
||||
msg.metaState = metaState;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createInjectText(String text) {
|
||||
ControlMessage event = new ControlMessage();
|
||||
event.type = TYPE_INJECT_TEXT;
|
||||
event.text = text;
|
||||
return event;
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_INJECT_TEXT;
|
||||
msg.text = text;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createInjectMotion(int action, int buttons, Position position) {
|
||||
ControlMessage event = new ControlMessage();
|
||||
event.type = TYPE_INJECT_MOUSE;
|
||||
event.action = action;
|
||||
event.buttons = buttons;
|
||||
event.position = position;
|
||||
return event;
|
||||
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_INJECT_TOUCH_EVENT;
|
||||
msg.action = action;
|
||||
msg.pointerId = pointerId;
|
||||
msg.pressure = pressure;
|
||||
msg.position = position;
|
||||
msg.buttons = buttons;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createInjectMotionTouch(int id, int action, Position position) {
|
||||
ControlMessage event = new ControlMessage();
|
||||
event.type = TYPE_INJECT_TOUCH;
|
||||
event.action = action;
|
||||
event.id = id;
|
||||
event.position = position;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static ControlMessage createInjectScroll(Position position, int hScroll, int vScroll) {
|
||||
ControlMessage event = new ControlMessage();
|
||||
event.type = TYPE_INJECT_SCROLL;
|
||||
event.position = position;
|
||||
event.hScroll = hScroll;
|
||||
event.vScroll = vScroll;
|
||||
return event;
|
||||
public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_INJECT_SCROLL_EVENT;
|
||||
msg.position = position;
|
||||
msg.hScroll = hScroll;
|
||||
msg.vScroll = vScroll;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createSetClipboard(String text) {
|
||||
ControlMessage event = new ControlMessage();
|
||||
event.type = TYPE_SET_CLIPBOARD;
|
||||
event.text = text;
|
||||
return event;
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_SET_CLIPBOARD;
|
||||
msg.text = text;
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants
|
||||
*/
|
||||
public static ControlMessage createSetScreenPowerMode(int mode) {
|
||||
ControlMessage event = new ControlMessage();
|
||||
event.type = TYPE_SET_SCREEN_POWER_MODE;
|
||||
event.action = mode;
|
||||
return event;
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_SET_SCREEN_POWER_MODE;
|
||||
msg.action = mode;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createEmpty(int type) {
|
||||
ControlMessage event = new ControlMessage();
|
||||
event.type = type;
|
||||
return event;
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = type;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
|
@ -123,8 +115,12 @@ public final class ControlMessage {
|
|||
return buttons;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
public long getPointerId() {
|
||||
return pointerId;
|
||||
}
|
||||
|
||||
public float getPressure() {
|
||||
return pressure;
|
||||
}
|
||||
|
||||
public Position getPosition() {
|
||||
|
|
|
@ -9,9 +9,9 @@ import java.nio.charset.StandardCharsets;
|
|||
public class ControlMessageReader {
|
||||
|
||||
private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9;
|
||||
private static final int INJECT_MOUSE_PAYLOAD_LENGTH = 13;
|
||||
private static final int INJECT_SCROLL_PAYLOAD_LENGTH = 16;
|
||||
private static final int INJECT_TOUCH_PAYLOAD_LENGTH = 10;
|
||||
private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17;
|
||||
private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21;
|
||||
private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
||||
private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
||||
|
||||
public static final int TEXT_MAX_LENGTH = 300;
|
||||
|
@ -50,6 +50,7 @@ public class ControlMessageReader {
|
|||
return null;
|
||||
}
|
||||
int savedPosition = buffer.position();
|
||||
|
||||
int type = buffer.get();
|
||||
ControlMessage msg;
|
||||
switch (type) {
|
||||
|
@ -59,14 +60,11 @@ public class ControlMessageReader {
|
|||
case ControlMessage.TYPE_INJECT_TEXT:
|
||||
msg = parseInjectText();
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_MOUSE:
|
||||
msg = parseInjectMouse();
|
||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||
msg = parseInjectTouchEvent();
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TOUCH:
|
||||
msg = parseInjectMouseTouch();
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_SCROLL:
|
||||
msg = parseInjectScroll();
|
||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||
msg = parseInjectScrollEvent();
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||
msg = parseSetClipboard();
|
||||
|
@ -78,6 +76,7 @@ public class ControlMessageReader {
|
|||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
msg = ControlMessage.createEmpty(type);
|
||||
break;
|
||||
default:
|
||||
|
@ -123,34 +122,30 @@ public class ControlMessageReader {
|
|||
return ControlMessage.createInjectText(text);
|
||||
}
|
||||
|
||||
private ControlMessage parseInjectMouse() {
|
||||
if (buffer.remaining() < INJECT_MOUSE_PAYLOAD_LENGTH) {
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private ControlMessage parseInjectTouchEvent() {
|
||||
if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
int action = toUnsigned(buffer.get());
|
||||
long pointerId = buffer.getLong();
|
||||
Position position = readPosition(buffer);
|
||||
// 16 bits fixed-point
|
||||
int pressureInt = toUnsigned(buffer.getShort());
|
||||
// convert it to a float between 0 and 1 (0x1p16f is 2^16 as float)
|
||||
float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f);
|
||||
int buttons = buffer.getInt();
|
||||
Position position = readPosition(buffer);
|
||||
return ControlMessage.createInjectMotion(action, buttons, position);
|
||||
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
|
||||
}
|
||||
|
||||
private ControlMessage parseInjectMouseTouch() {
|
||||
if (buffer.remaining() < INJECT_TOUCH_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
int id = toUnsigned(buffer.get());
|
||||
int action = toUnsigned(buffer.get());
|
||||
Position position = readPosition(buffer);
|
||||
return ControlMessage.createInjectMotionTouch(id, action, position);
|
||||
}
|
||||
|
||||
private ControlMessage parseInjectScroll() {
|
||||
if (buffer.remaining() < INJECT_SCROLL_PAYLOAD_LENGTH) {
|
||||
private ControlMessage parseInjectScrollEvent() {
|
||||
if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
Position position = readPosition(buffer);
|
||||
int hScroll = buffer.getInt();
|
||||
int vScroll = buffer.getInt();
|
||||
return ControlMessage.createInjectScroll(position, hScroll, vScroll);
|
||||
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll);
|
||||
}
|
||||
|
||||
private ControlMessage parseSetClipboard() {
|
||||
|
@ -170,8 +165,8 @@ public class ControlMessageReader {
|
|||
}
|
||||
|
||||
private static Position readPosition(ByteBuffer buffer) {
|
||||
int x = toUnsigned(buffer.getShort());
|
||||
int y = toUnsigned(buffer.getShort());
|
||||
int x = buffer.getInt();
|
||||
int y = buffer.getInt();
|
||||
int screenWidth = toUnsigned(buffer.getShort());
|
||||
int screenHeight = toUnsigned(buffer.getShort());
|
||||
return new Position(x, y, screenWidth, screenHeight);
|
||||
|
|
|
@ -2,7 +2,6 @@ package com.genymobile.scrcpy;
|
|||
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.os.SystemClock;
|
||||
import android.view.InputDevice;
|
||||
import android.view.InputEvent;
|
||||
|
@ -11,95 +10,41 @@ import android.view.KeyEvent;
|
|||
import android.view.MotionEvent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Vector;
|
||||
|
||||
public class Controller {
|
||||
|
||||
private static final int DEVICE_ID_VIRTUAL = -1;
|
||||
|
||||
private final Device device;
|
||||
private final DesktopConnection connection;
|
||||
private final DeviceMessageSender sender;
|
||||
|
||||
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
|
||||
|
||||
private long lastMouseDown;
|
||||
private Vector<MotionEvent.PointerProperties> pointerProperties = new Vector<MotionEvent.PointerProperties>();
|
||||
private Vector<MotionEvent.PointerCoords> pointerCoords = new Vector<MotionEvent.PointerCoords>();
|
||||
private long lastTouchDown;
|
||||
private final PointersState pointersState = new PointersState();
|
||||
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
|
||||
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
|
||||
|
||||
public Controller(Device device, DesktopConnection connection) {
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
initPointers();
|
||||
sender = new DeviceMessageSender(connection);
|
||||
}
|
||||
|
||||
private int getPointer(int id) {
|
||||
for (int i = 0; i < pointerProperties.size(); i++) {
|
||||
if (id == pointerProperties.get(i).id) {
|
||||
return i;
|
||||
}
|
||||
private void initPointers() {
|
||||
for (int i = 0; i < PointersState.MAX_POINTERS; ++i) {
|
||||
MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
|
||||
props.toolType = MotionEvent.TOOL_TYPE_FINGER;
|
||||
|
||||
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
|
||||
coords.orientation = 0;
|
||||
coords.size = 1;
|
||||
|
||||
pointerProperties[i] = props;
|
||||
pointerCoords[i] = coords;
|
||||
}
|
||||
|
||||
MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
|
||||
props.id = id;
|
||||
props.toolType = MotionEvent.TOOL_TYPE_FINGER;
|
||||
pointerProperties.addElement(props);
|
||||
|
||||
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
|
||||
coords.orientation = 0;
|
||||
coords.pressure = 1;
|
||||
coords.size = 1;
|
||||
pointerCoords.addElement(coords);
|
||||
return pointerProperties.size() - 1;
|
||||
}
|
||||
|
||||
private void releasePointer(int id) {
|
||||
int index = -1;
|
||||
for (int i = 0; i < pointerProperties.size(); i++) {
|
||||
if (id == pointerProperties.get(i).id) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( -1 != index) {
|
||||
pointerProperties.remove(index);
|
||||
pointerCoords.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPointerCoords(int id, Point point) {
|
||||
int index = -1;
|
||||
for (int i = 0; i < pointerProperties.size(); i++) {
|
||||
if (id == pointerProperties.get(i).id) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( -1 != index) {
|
||||
MotionEvent.PointerCoords coords = pointerCoords.get(index);
|
||||
coords.x = point.x;
|
||||
coords.y = point.y;
|
||||
}
|
||||
}
|
||||
|
||||
private void setScroll(int id, int hScroll, int vScroll) {
|
||||
int index = -1;
|
||||
for (int i = 0; i < pointerProperties.size(); i++) {
|
||||
if (id == pointerProperties.get(i).id) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( -1 != index) {
|
||||
MotionEvent.PointerCoords coords = pointerCoords.get(index);
|
||||
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
|
||||
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
||||
}
|
||||
}
|
||||
|
||||
public DeviceMessageSender getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
|
@ -123,6 +68,10 @@ public class Controller {
|
|||
}
|
||||
}
|
||||
|
||||
public DeviceMessageSender getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
private void handleEvent() throws IOException {
|
||||
ControlMessage msg = connection.receiveControlMessage();
|
||||
switch (msg.getType()) {
|
||||
|
@ -132,13 +81,10 @@ public class Controller {
|
|||
case ControlMessage.TYPE_INJECT_TEXT:
|
||||
injectText(msg.getText());
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_MOUSE:
|
||||
injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition());
|
||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TOUCH:
|
||||
injectTouch(msg.getId(), msg.getAction(), msg.getPosition());
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_SCROLL:
|
||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
|
||||
break;
|
||||
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
||||
|
@ -160,6 +106,9 @@ public class Controller {
|
|||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||
device.setScreenPowerMode(msg.getAction());
|
||||
break;
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
device.rotateDevice();
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
|
@ -196,87 +145,43 @@ public class Controller {
|
|||
return successCount;
|
||||
}
|
||||
|
||||
private boolean injectTouch(int id, int action, Position position) {
|
||||
if (action != MotionEvent.ACTION_DOWN
|
||||
&& action != MotionEvent.ACTION_UP
|
||||
&& action != MotionEvent.ACTION_MOVE) {
|
||||
Ln.w("Unsupported action: " + action);
|
||||
return false;
|
||||
}
|
||||
if (id < 0 || id > 9) {
|
||||
Ln.w("Unsupported id[0-9]: " + id);
|
||||
return false;
|
||||
}
|
||||
|
||||
int index = getPointer(id);
|
||||
int convertAction = action;
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
if (1 != pointerProperties.size()) {
|
||||
convertAction = (index << 8) | MotionEvent.ACTION_POINTER_DOWN;
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (1 != pointerProperties.size()) {
|
||||
convertAction = (index << 8) | convertAction;
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (1 != pointerProperties.size()) {
|
||||
convertAction = (index << 8) | MotionEvent.ACTION_POINTER_UP;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Point point = device.getPhysicalPoint(position);
|
||||
if (point == null) {
|
||||
// ignore event
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pointerProperties.isEmpty()) {
|
||||
// ignore event
|
||||
return false;
|
||||
}
|
||||
setPointerCoords(id, point);
|
||||
MotionEvent.PointerProperties[] props = pointerProperties.toArray(new MotionEvent.PointerProperties[pointerProperties.size()]);
|
||||
MotionEvent.PointerCoords[] coords = pointerCoords.toArray(new MotionEvent.PointerCoords[pointerCoords.size()]);
|
||||
MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), convertAction,
|
||||
pointerProperties.size(), props, coords, 0, 0, 1f, 1f, 0, 0,
|
||||
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
||||
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
releasePointer(id);
|
||||
}
|
||||
return injectEvent(event);
|
||||
}
|
||||
|
||||
private boolean injectMouse(int action, int buttons, Position position) {
|
||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
getPointer(0);
|
||||
lastMouseDown = now;
|
||||
}
|
||||
|
||||
Point point = device.getPhysicalPoint(position);
|
||||
if (point == null) {
|
||||
// ignore event
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pointerProperties.isEmpty()) {
|
||||
// ignore event
|
||||
int pointerIndex = pointersState.getPointerIndex(pointerId);
|
||||
if (pointerIndex == -1) {
|
||||
Ln.w("Too many pointers for touch event");
|
||||
return false;
|
||||
}
|
||||
setPointerCoords(0, point);
|
||||
MotionEvent.PointerProperties[] props = pointerProperties.toArray(new MotionEvent.PointerProperties[pointerProperties.size()]);
|
||||
MotionEvent.PointerCoords[] coords = pointerCoords.toArray(new MotionEvent.PointerCoords[pointerCoords.size()]);
|
||||
MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action,
|
||||
pointerProperties.size(), props, coords, 0, buttons, 1f, 1f, 0, 0,
|
||||
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
||||
Pointer pointer = pointersState.get(pointerIndex);
|
||||
pointer.setPoint(point);
|
||||
pointer.setPressure(pressure);
|
||||
pointer.setUp(action == MotionEvent.ACTION_UP);
|
||||
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
releasePointer(0);
|
||||
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
|
||||
|
||||
if (pointerCount == 1) {
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
lastTouchDown = now;
|
||||
}
|
||||
} else {
|
||||
// secondary pointers must use ACTION_POINTER_* ORed with the pointerIndex
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
action = MotionEvent.ACTION_POINTER_UP | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
|
||||
} else if (action == MotionEvent.ACTION_DOWN) {
|
||||
action = MotionEvent.ACTION_POINTER_DOWN | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent event = MotionEvent
|
||||
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
||||
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
||||
return injectEvent(event);
|
||||
}
|
||||
|
||||
|
@ -288,23 +193,18 @@ public class Controller {
|
|||
return false;
|
||||
}
|
||||
|
||||
// init
|
||||
MotionEvent.PointerProperties[] props = {new MotionEvent.PointerProperties()};
|
||||
props[0].id = 0;
|
||||
props[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
|
||||
MotionEvent.PointerCoords[] coords = {new MotionEvent.PointerCoords()};
|
||||
coords[0].orientation = 0;
|
||||
coords[0].pressure = 1;
|
||||
coords[0].size = 1;
|
||||
MotionEvent.PointerProperties props = pointerProperties[0];
|
||||
props.id = 0;
|
||||
|
||||
// set data
|
||||
coords[0].x = point.x;
|
||||
coords[0].y = point.y;
|
||||
coords[0].setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
|
||||
coords[0].setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
||||
MotionEvent.PointerCoords coords = pointerCoords[0];
|
||||
coords.x = point.getX();
|
||||
coords.y = point.getY();
|
||||
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
|
||||
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
||||
|
||||
MotionEvent event = MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, props, coords, 0, 0, 1f, 1f, 0,
|
||||
0, InputDevice.SOURCE_MOUSE, 0);
|
||||
MotionEvent event = MotionEvent
|
||||
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
||||
InputDevice.SOURCE_MOUSE, 0);
|
||||
return injectEvent(event);
|
||||
}
|
||||
|
||||
|
@ -316,8 +216,7 @@ public class Controller {
|
|||
}
|
||||
|
||||
private boolean injectKeycode(int keyCode) {
|
||||
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0)
|
||||
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
|
||||
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
|
||||
}
|
||||
|
||||
private boolean injectEvent(InputEvent event) {
|
||||
|
|
|
@ -15,7 +15,7 @@ public final class DesktopConnection implements Closeable {
|
|||
|
||||
private static final int DEVICE_NAME_FIELD_LENGTH = 64;
|
||||
|
||||
private static final String SOCKET_NAME = "qtscrcpy";
|
||||
private static final String SOCKET_NAME = "scrcpy";
|
||||
|
||||
private final LocalSocket videoSocket;
|
||||
private final FileDescriptor videoFd;
|
||||
|
@ -24,7 +24,6 @@ public final class DesktopConnection implements Closeable {
|
|||
private final InputStream controlInputStream;
|
||||
private final OutputStream controlOutputStream;
|
||||
|
||||
|
||||
private final ControlMessageReader reader = new ControlMessageReader();
|
||||
private final DeviceMessageWriter writer = new DeviceMessageWriter();
|
||||
|
||||
|
@ -90,7 +89,7 @@ public final class DesktopConnection implements Closeable {
|
|||
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
|
||||
|
||||
byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
|
||||
int len = Math.min(DEVICE_NAME_FIELD_LENGTH - 1, deviceNameBytes.length);
|
||||
int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1);
|
||||
System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
|
||||
// byte[] are always 0-initialized in java, no need to set '\0' explicitly
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@ package com.genymobile.scrcpy;
|
|||
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||
import com.genymobile.scrcpy.wrappers.WindowManager;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
@ -74,7 +74,6 @@ public final class Device {
|
|||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private static Size computeVideoSize(int w, int h, int maxSize) {
|
||||
// Compute the video size and the padding of the content inside this video.
|
||||
/*
|
||||
// Principle:
|
||||
// - scale down the great side of the screen to maxSize (if necessary);
|
||||
// - scale down the other side so that the aspect ratio is preserved;
|
||||
|
@ -97,46 +96,7 @@ public final class Device {
|
|||
w = portrait ? minor : major;
|
||||
h = portrait ? major : minor;
|
||||
}
|
||||
*/
|
||||
|
||||
// Principle:480p/720p/1080p and not larger than device size.
|
||||
w &= ~7; // in case it's not a multiple of 8
|
||||
h &= ~7;
|
||||
boolean vertival = h > w;
|
||||
boolean validSize = false;
|
||||
int newWidth = w;
|
||||
int newHeight = h;
|
||||
// 480p/720p/1080p
|
||||
switch (maxSize) {
|
||||
case 480: // 480p:640x480
|
||||
newWidth = 640;
|
||||
newHeight = 480;
|
||||
validSize = true;
|
||||
break;
|
||||
case 720: // 720p:1280x720
|
||||
newWidth = 1280;
|
||||
newHeight = 720;
|
||||
validSize = true;
|
||||
break;
|
||||
case 1080: // 1080p:1920x1080
|
||||
newWidth = 1920;
|
||||
newHeight = 1080;
|
||||
validSize = true;
|
||||
break;
|
||||
}
|
||||
// vertival convert
|
||||
if (validSize && vertival) {
|
||||
int temp = newWidth;
|
||||
newWidth = newHeight;
|
||||
newHeight = temp;
|
||||
}
|
||||
// not larger than device size.
|
||||
if (newWidth > w || newHeight > h) {
|
||||
newWidth = w;
|
||||
newHeight = h;
|
||||
}
|
||||
|
||||
return new Size(newWidth, newHeight);
|
||||
return new Size(w, h);
|
||||
}
|
||||
|
||||
public Point getPhysicalPoint(Position position) {
|
||||
|
@ -152,8 +112,8 @@ public final class Device {
|
|||
}
|
||||
Rect contentRect = screenInfo.getContentRect();
|
||||
Point point = position.getPoint();
|
||||
int scaledX = contentRect.left + point.x * contentRect.width() / videoSize.getWidth();
|
||||
int scaledY = contentRect.top + point.y * contentRect.height() / videoSize.getHeight();
|
||||
int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth();
|
||||
int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight();
|
||||
return new Point(scaledX, scaledY);
|
||||
}
|
||||
|
||||
|
@ -202,9 +162,34 @@ public final class Device {
|
|||
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
|
||||
*/
|
||||
public void setScreenPowerMode(int mode) {
|
||||
IBinder d = SurfaceControl.getBuiltInDisplay(0);
|
||||
IBinder d = SurfaceControl.getBuiltInDisplay();
|
||||
if (d == null) {
|
||||
Ln.e("Could not get built-in display");
|
||||
return;
|
||||
}
|
||||
SurfaceControl.setDisplayPowerMode(d, mode);
|
||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off " : "on ") + mode);
|
||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
|
||||
*/
|
||||
public void rotateDevice() {
|
||||
WindowManager wm = serviceManager.getWindowManager();
|
||||
|
||||
boolean accelerometerRotation = !wm.isRotationFrozen();
|
||||
|
||||
int currentRotation = wm.getRotation();
|
||||
int newRotation = (currentRotation & 1) ^ 1; // 0->1, 1->0, 2->1, 3->0
|
||||
String newRotationString = newRotation == 0 ? "portrait" : "landscape";
|
||||
|
||||
Ln.i("Device rotation requested: " + newRotationString);
|
||||
wm.freezeRotation(newRotation);
|
||||
|
||||
// restore auto-rotate if necessary
|
||||
if (accelerometerRotation) {
|
||||
wm.thawRotation();
|
||||
}
|
||||
}
|
||||
|
||||
static Rect flipRect(Rect crop) {
|
||||
|
|
|
@ -27,8 +27,8 @@ public final class DeviceMessageSender {
|
|||
text = clipboardText;
|
||||
clipboardText = null;
|
||||
}
|
||||
DeviceMessage msg = DeviceMessage.createClipboard(text);
|
||||
connection.sendDeviceMessage(msg);
|
||||
DeviceMessage event = DeviceMessage.createClipboard(text);
|
||||
connection.sendDeviceMessage(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ public class DeviceMessageWriter {
|
|||
output.write(rawBuffer, 0, buffer.position());
|
||||
break;
|
||||
default:
|
||||
Ln.w("Unknown device msg: " + msg.getType());
|
||||
Ln.w("Unknown device message: " + msg.getType());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,14 +8,11 @@ import android.util.Log;
|
|||
*/
|
||||
public final class Ln {
|
||||
|
||||
private static final String TAG = "qtscrcpy";
|
||||
private static final String TAG = "scrcpy";
|
||||
private static final String PREFIX = "[server] ";
|
||||
|
||||
enum Level {
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR;
|
||||
DEBUG, INFO, WARN, ERROR
|
||||
}
|
||||
|
||||
private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO;
|
||||
|
|
|
@ -5,9 +5,10 @@ import android.graphics.Rect;
|
|||
public class Options {
|
||||
private int maxSize;
|
||||
private int bitRate;
|
||||
private int maxFps;
|
||||
private boolean tunnelForward;
|
||||
private Rect crop;
|
||||
private boolean sendFrameMeta;
|
||||
private boolean sendFrameMeta; // send PTS so that the client may record properly
|
||||
private boolean control;
|
||||
|
||||
public int getMaxSize() {
|
||||
|
@ -26,6 +27,14 @@ public class Options {
|
|||
this.bitRate = bitRate;
|
||||
}
|
||||
|
||||
public int getMaxFps() {
|
||||
return maxFps;
|
||||
}
|
||||
|
||||
public void setMaxFps(int maxFps) {
|
||||
this.maxFps = maxFps;
|
||||
}
|
||||
|
||||
public boolean isTunnelForward() {
|
||||
return tunnelForward;
|
||||
}
|
||||
|
|
43
server/src/main/java/com/genymobile/scrcpy/Point.java
Normal file
43
server/src/main/java/com/genymobile/scrcpy/Point.java
Normal file
|
@ -0,0 +1,43 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Point {
|
||||
private final int x;
|
||||
private final int y;
|
||||
|
||||
public Point(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Point point = (Point) o;
|
||||
return x == point.x && y == point.y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Point{" + "x=" + x + ", y=" + y + '}';
|
||||
}
|
||||
}
|
55
server/src/main/java/com/genymobile/scrcpy/Pointer.java
Normal file
55
server/src/main/java/com/genymobile/scrcpy/Pointer.java
Normal file
|
@ -0,0 +1,55 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
public class Pointer {
|
||||
|
||||
/**
|
||||
* Pointer id as received from the client.
|
||||
*/
|
||||
private final long id;
|
||||
|
||||
/**
|
||||
* Local pointer id, using the lowest possible values to fill the {@link android.view.MotionEvent.PointerProperties PointerProperties}.
|
||||
*/
|
||||
private final int localId;
|
||||
|
||||
private Point point;
|
||||
private float pressure;
|
||||
private boolean up;
|
||||
|
||||
public Pointer(long id, int localId) {
|
||||
this.id = id;
|
||||
this.localId = localId;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public int getLocalId() {
|
||||
return localId;
|
||||
}
|
||||
|
||||
public Point getPoint() {
|
||||
return point;
|
||||
}
|
||||
|
||||
public void setPoint(Point point) {
|
||||
this.point = point;
|
||||
}
|
||||
|
||||
public float getPressure() {
|
||||
return pressure;
|
||||
}
|
||||
|
||||
public void setPressure(float pressure) {
|
||||
this.pressure = pressure;
|
||||
}
|
||||
|
||||
public boolean isUp() {
|
||||
return up;
|
||||
}
|
||||
|
||||
public void setUp(boolean up) {
|
||||
this.up = up;
|
||||
}
|
||||
}
|
103
server/src/main/java/com/genymobile/scrcpy/PointersState.java
Normal file
103
server/src/main/java/com/genymobile/scrcpy/PointersState.java
Normal file
|
@ -0,0 +1,103 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PointersState {
|
||||
|
||||
public static final int MAX_POINTERS = 10;
|
||||
|
||||
private final List<Pointer> pointers = new ArrayList<>();
|
||||
|
||||
private int indexOf(long id) {
|
||||
for (int i = 0; i < pointers.size(); ++i) {
|
||||
Pointer pointer = pointers.get(i);
|
||||
if (pointer.getId() == id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private boolean isLocalIdAvailable(int localId) {
|
||||
for (int i = 0; i < pointers.size(); ++i) {
|
||||
Pointer pointer = pointers.get(i);
|
||||
if (pointer.getLocalId() == localId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private int nextUnusedLocalId() {
|
||||
for (int localId = 0; localId < MAX_POINTERS; ++localId) {
|
||||
if (isLocalIdAvailable(localId)) {
|
||||
return localId;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public Pointer get(int index) {
|
||||
return pointers.get(index);
|
||||
}
|
||||
|
||||
public int getPointerIndex(long id) {
|
||||
int index = indexOf(id);
|
||||
if (index != -1) {
|
||||
// already exists, return it
|
||||
return index;
|
||||
}
|
||||
if (pointers.size() >= MAX_POINTERS) {
|
||||
// it's full
|
||||
return -1;
|
||||
}
|
||||
// id 0 is reserved for mouse events
|
||||
int localId = nextUnusedLocalId();
|
||||
if (localId == -1) {
|
||||
throw new AssertionError("pointers.size() < maxFingers implies that a local id is available");
|
||||
}
|
||||
Pointer pointer = new Pointer(id, localId);
|
||||
pointers.add(pointer);
|
||||
// return the index of the pointer
|
||||
return pointers.size() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the motion event parameters.
|
||||
*
|
||||
* @param props the pointer properties
|
||||
* @param coords the pointer coordinates
|
||||
* @return The number of items initialized (the number of pointers).
|
||||
*/
|
||||
public int update(MotionEvent.PointerProperties[] props, MotionEvent.PointerCoords[] coords) {
|
||||
int count = pointers.size();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Pointer pointer = pointers.get(i);
|
||||
|
||||
// id 0 is reserved for mouse events
|
||||
props[i].id = pointer.getLocalId();
|
||||
|
||||
Point point = pointer.getPoint();
|
||||
coords[i].x = point.getX();
|
||||
coords[i].y = point.getY();
|
||||
coords[i].pressure = pointer.getPressure();
|
||||
}
|
||||
cleanUp();
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all pointers which are UP.
|
||||
*/
|
||||
private void cleanUp() {
|
||||
for (int i = pointers.size() - 1; i >= 0; --i) {
|
||||
Pointer pointer = pointers.get(i);
|
||||
if (pointer.isUp()) {
|
||||
pointers.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Point;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Position {
|
||||
|
@ -34,8 +32,7 @@ public class Position {
|
|||
return false;
|
||||
}
|
||||
Position position = (Position) o;
|
||||
return Objects.equals(point, position.point)
|
||||
&& Objects.equals(screenSize, position.screenSize);
|
||||
return Objects.equals(point, position.point) && Objects.equals(screenSize, position.screenSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -45,10 +42,7 @@ public class Position {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Position{"
|
||||
+ "point=" + point
|
||||
+ ", screenSize=" + screenSize
|
||||
+ '}';
|
||||
return "Position{" + "point=" + point + ", screenSize=" + screenSize + '}';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.graphics.Rect;
|
|||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.view.Surface;
|
||||
|
||||
|
@ -16,32 +17,29 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
|
||||
public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
private static final int DEFAULT_FRAME_RATE = 60; // fps
|
||||
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
|
||||
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
|
||||
|
||||
private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames
|
||||
|
||||
private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000;
|
||||
private static final int NO_PTS = -1;
|
||||
|
||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||
|
||||
private int bitRate;
|
||||
private int frameRate;
|
||||
private int maxFps;
|
||||
private int iFrameInterval;
|
||||
private boolean sendFrameMeta;
|
||||
private long ptsOrigin;
|
||||
|
||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int frameRate, int iFrameInterval) {
|
||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) {
|
||||
this.sendFrameMeta = sendFrameMeta;
|
||||
this.bitRate = bitRate;
|
||||
this.frameRate = frameRate;
|
||||
this.maxFps = maxFps;
|
||||
this.iFrameInterval = iFrameInterval;
|
||||
}
|
||||
|
||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate) {
|
||||
this(sendFrameMeta, bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
|
||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
|
||||
this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -54,7 +52,10 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||
}
|
||||
|
||||
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||
MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
|
||||
Workarounds.prepareMainLooper();
|
||||
Workarounds.fillAppInfo();
|
||||
|
||||
MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval);
|
||||
device.setRotationListener(this);
|
||||
boolean alive;
|
||||
try {
|
||||
|
@ -87,7 +88,6 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||
boolean eof = false;
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
|
||||
while (!consumeRotationChange() && !eof) {
|
||||
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
|
||||
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
|
||||
|
@ -138,15 +138,24 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||
return MediaCodec.createEncoderByType("video/avc");
|
||||
}
|
||||
|
||||
private static MediaFormat createFormat(int bitRate, int frameRate, int iFrameInterval) throws IOException {
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) {
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, "video/avc");
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
|
||||
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
|
||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
|
||||
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
|
||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
|
||||
// display the very first frame, and recover from bad quality when no new frames
|
||||
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate); // µs
|
||||
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
|
||||
if (maxFps > 0) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
format.setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, maxFps);
|
||||
} else {
|
||||
Ln.w("Max FPS is only supported since Android 10, the option has been ignored");
|
||||
}
|
||||
}
|
||||
return format;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class Server {
|
||||
|
@ -18,7 +19,7 @@ public final class Server {
|
|||
final Device device = new Device(options);
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
|
||||
|
||||
if (options.getControl()) {
|
||||
Controller controller = new Controller(device, connection);
|
||||
|
@ -60,7 +61,7 @@ public final class Server {
|
|||
sender.loop();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Devide message sender stopped");
|
||||
Ln.d("Device message sender stopped");
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
@ -68,29 +69,42 @@ public final class Server {
|
|||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private static Options createOptions(String... args) {
|
||||
if (args.length != 6) {
|
||||
throw new IllegalArgumentException("Expecting 5 parameters");
|
||||
if (args.length < 1) {
|
||||
throw new IllegalArgumentException("Missing client version");
|
||||
}
|
||||
|
||||
String clientVersion = args[0];
|
||||
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
|
||||
throw new IllegalArgumentException(
|
||||
"The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")");
|
||||
}
|
||||
|
||||
if (args.length != 8) {
|
||||
throw new IllegalArgumentException("Expecting 8 parameters");
|
||||
}
|
||||
|
||||
Options options = new Options();
|
||||
|
||||
int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8
|
||||
int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8
|
||||
options.setMaxSize(maxSize);
|
||||
|
||||
int bitRate = Integer.parseInt(args[1]);
|
||||
int bitRate = Integer.parseInt(args[2]);
|
||||
options.setBitRate(bitRate);
|
||||
|
||||
int maxFps = Integer.parseInt(args[3]);
|
||||
options.setMaxFps(maxFps);
|
||||
|
||||
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
|
||||
boolean tunnelForward = Boolean.parseBoolean(args[2]);
|
||||
boolean tunnelForward = Boolean.parseBoolean(args[4]);
|
||||
options.setTunnelForward(tunnelForward);
|
||||
|
||||
Rect crop = parseCrop(args[3]);
|
||||
Rect crop = parseCrop(args[5]);
|
||||
options.setCrop(crop);
|
||||
|
||||
boolean sendFrameMeta = Boolean.parseBoolean(args[4]);
|
||||
boolean sendFrameMeta = Boolean.parseBoolean(args[6]);
|
||||
options.setSendFrameMeta(sendFrameMeta);
|
||||
|
||||
boolean control = Boolean.parseBoolean(args[5]);
|
||||
boolean control = Boolean.parseBoolean(args[7]);
|
||||
options.setControl(control);
|
||||
|
||||
return options;
|
||||
|
@ -117,7 +131,21 @@ public final class Server {
|
|||
try {
|
||||
new File(SERVER_PATH).delete();
|
||||
} catch (Exception e) {
|
||||
Ln.e("Cannot unlink server", e);
|
||||
Ln.e("Could not unlink server", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private static void suggestFix(Throwable e) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (e instanceof MediaCodec.CodecException) {
|
||||
MediaCodec.CodecException mce = (MediaCodec.CodecException) e;
|
||||
if (mce.getErrorCode() == 0xfffffc0e) {
|
||||
Ln.e("The hardware encoder is not able to encode at the given definition.");
|
||||
Ln.e("Try with a lower definition:");
|
||||
Ln.e(" scrcpy -m 1024");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,6 +154,7 @@ public final class Server {
|
|||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
Ln.e("Exception on thread " + t, e);
|
||||
suggestFix(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -38,8 +38,7 @@ public final class Size {
|
|||
return false;
|
||||
}
|
||||
Size size = (Size) o;
|
||||
return width == size.width
|
||||
&& height == size.height;
|
||||
return width == size.width && height == size.height;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -49,9 +48,6 @@ public final class Size {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Size{"
|
||||
+ "width=" + width
|
||||
+ ", height=" + height
|
||||
+ '}';
|
||||
return "Size{" + "width=" + width + ", height=" + height + '}';
|
||||
}
|
||||
}
|
||||
|
|
79
server/src/main/java/com/genymobile/scrcpy/Workarounds.java
Normal file
79
server/src/main/java/com/genymobile/scrcpy/Workarounds.java
Normal file
|
@ -0,0 +1,79 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class Workarounds {
|
||||
private Workarounds() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static void prepareMainLooper() {
|
||||
// Some devices internally create a Handler when creating an input Surface, causing an exception:
|
||||
// "Can't create handler inside thread that has not called Looper.prepare()"
|
||||
// <https://github.com/Genymobile/scrcpy/issues/240>
|
||||
//
|
||||
// Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException:
|
||||
// "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue'
|
||||
// on a null object reference"
|
||||
// <https://github.com/Genymobile/scrcpy/issues/921>
|
||||
Looper.prepareMainLooper();
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
public static void fillAppInfo() {
|
||||
try {
|
||||
// ActivityThread activityThread = new ActivityThread();
|
||||
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
|
||||
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
|
||||
activityThreadConstructor.setAccessible(true);
|
||||
Object activityThread = activityThreadConstructor.newInstance();
|
||||
|
||||
// ActivityThread.sCurrentActivityThread = activityThread;
|
||||
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
|
||||
sCurrentActivityThreadField.setAccessible(true);
|
||||
sCurrentActivityThreadField.set(null, activityThread);
|
||||
|
||||
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
|
||||
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
|
||||
Constructor<?> appBindDataConstructor = appBindDataClass.getDeclaredConstructor();
|
||||
appBindDataConstructor.setAccessible(true);
|
||||
Object appBindData = appBindDataConstructor.newInstance();
|
||||
|
||||
ApplicationInfo applicationInfo = new ApplicationInfo();
|
||||
applicationInfo.packageName = "com.genymobile.scrcpy";
|
||||
|
||||
// appBindData.appInfo = applicationInfo;
|
||||
Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
|
||||
appInfoField.setAccessible(true);
|
||||
appInfoField.set(appBindData, applicationInfo);
|
||||
|
||||
// activityThread.mBoundApplication = appBindData;
|
||||
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
|
||||
mBoundApplicationField.setAccessible(true);
|
||||
mBoundApplicationField.set(activityThread, appBindData);
|
||||
|
||||
// Context ctx = activityThread.getSystemContext();
|
||||
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
|
||||
Context ctx = (Context) getSystemContextMethod.invoke(activityThread);
|
||||
|
||||
Application app = Instrumentation.newApplication(Application.class, ctx);
|
||||
|
||||
// activityThread.mInitialApplication = app;
|
||||
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
|
||||
mInitialApplicationField.setAccessible(true);
|
||||
mInitialApplicationField.set(activityThread, app);
|
||||
} catch (Throwable throwable) {
|
||||
// this is a workaround, so failing is not an error
|
||||
Ln.w("Could not fill app info: " + throwable.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +1,86 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class ClipboardManager {
|
||||
|
||||
private static final String PACKAGE_NAME = "com.android.shell";
|
||||
private static final int USER_ID = 0;
|
||||
|
||||
private final IInterface manager;
|
||||
private final Method getPrimaryClipMethod;
|
||||
private final Method setPrimaryClipMethod;
|
||||
private Method getPrimaryClipMethod;
|
||||
private 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);
|
||||
}
|
||||
|
||||
private Method getGetPrimaryClipMethod() throws NoSuchMethodException {
|
||||
if (getPrimaryClipMethod == null) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
|
||||
} else {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
|
||||
}
|
||||
}
|
||||
return getPrimaryClipMethod;
|
||||
}
|
||||
|
||||
private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
|
||||
if (setPrimaryClipMethod == null) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
|
||||
} else {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
|
||||
}
|
||||
}
|
||||
return setPrimaryClipMethod;
|
||||
}
|
||||
|
||||
private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
return (ClipData) method.invoke(manager, PACKAGE_NAME);
|
||||
}
|
||||
return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID);
|
||||
}
|
||||
|
||||
private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
method.invoke(manager, clipData, PACKAGE_NAME);
|
||||
} else {
|
||||
method.invoke(manager, clipData, PACKAGE_NAME, USER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
public CharSequence getText() {
|
||||
try {
|
||||
ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell");
|
||||
Method method = getGetPrimaryClipMethod();
|
||||
ClipData clipData = getPrimaryClip(method, manager);
|
||||
if (clipData == null || clipData.getItemCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
return clipData.getItemAt(0).getText();
|
||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
Method method = getSetPrimaryClipMethod();
|
||||
ClipData clipData = ClipData.newPlainText(null, text);
|
||||
setPrimaryClip(method, manager, clipData);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.os.IInterface;
|
||||
import android.view.InputEvent;
|
||||
|
||||
|
@ -13,22 +15,26 @@ public final class InputManager {
|
|||
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
|
||||
|
||||
private final IInterface manager;
|
||||
private final Method injectInputEventMethod;
|
||||
private Method injectInputEventMethod;
|
||||
|
||||
public InputManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
try {
|
||||
}
|
||||
|
||||
private Method getInjectInputEventMethod() throws NoSuchMethodException {
|
||||
if (injectInputEventMethod == null) {
|
||||
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return injectInputEventMethod;
|
||||
}
|
||||
|
||||
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
|
||||
try {
|
||||
return (Boolean) injectInputEventMethod.invoke(manager, inputEvent, mode);
|
||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||
throw new AssertionError(e);
|
||||
Method method = getInjectInputEventMethod();
|
||||
return (boolean) method.invoke(manager, inputEvent, mode);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
|
@ -9,24 +11,28 @@ import java.lang.reflect.Method;
|
|||
|
||||
public final class PowerManager {
|
||||
private final IInterface manager;
|
||||
private final Method isScreenOnMethod;
|
||||
private Method isScreenOnMethod;
|
||||
|
||||
public PowerManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
try {
|
||||
}
|
||||
|
||||
private Method getIsScreenOnMethod() throws NoSuchMethodException {
|
||||
if (isScreenOnMethod == null) {
|
||||
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
|
||||
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
|
||||
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
|
||||
isScreenOnMethod = manager.getClass().getMethod(methodName);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return isScreenOnMethod;
|
||||
}
|
||||
|
||||
public boolean isScreenOn() {
|
||||
try {
|
||||
return (Boolean) isScreenOnMethod.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||
throw new AssertionError(e);
|
||||
Method method = getIsScreenOnMethod();
|
||||
return (boolean) method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,35 +17,35 @@ public class StatusBarManager {
|
|||
this.manager = manager;
|
||||
}
|
||||
|
||||
public void expandNotificationsPanel() {
|
||||
private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException {
|
||||
if (expandNotificationsPanelMethod == null) {
|
||||
try {
|
||||
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
|
||||
} catch (NoSuchMethodException e) {
|
||||
Ln.e("ServiceBarManager.expandNotificationsPanel() is not available on this device");
|
||||
return;
|
||||
}
|
||||
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
|
||||
}
|
||||
return expandNotificationsPanelMethod;
|
||||
}
|
||||
|
||||
private Method getCollapsePanelsMethod() throws NoSuchMethodException {
|
||||
if (collapsePanelsMethod == null) {
|
||||
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
|
||||
}
|
||||
return collapsePanelsMethod;
|
||||
}
|
||||
|
||||
public void expandNotificationsPanel() {
|
||||
try {
|
||||
expandNotificationsPanelMethod.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||
Ln.e("Cannot invoke ServiceBarManager.expandNotificationsPanel()", e);
|
||||
Method method = getExpandNotificationsPanelMethod();
|
||||
method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void collapsePanels() {
|
||||
if (collapsePanelsMethod == null) {
|
||||
try {
|
||||
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
|
||||
} catch (NoSuchMethodException e) {
|
||||
Ln.e("ServiceBarManager.collapsePanels() is not available on this device");
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
collapsePanelsMethod.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||
Ln.e("Cannot invoke ServiceBarManager.collapsePanels()", e);
|
||||
Method method = getCollapsePanelsMethod();
|
||||
method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
public final class SurfaceControl {
|
||||
|
||||
|
@ -23,6 +28,9 @@ public final class SurfaceControl {
|
|||
}
|
||||
}
|
||||
|
||||
private static Method getBuiltInDisplayMethod;
|
||||
private static Method setDisplayPowerModeMethod;
|
||||
|
||||
private SurfaceControl() {
|
||||
// only static methods
|
||||
}
|
||||
|
@ -76,24 +84,49 @@ public final class SurfaceControl {
|
|||
}
|
||||
}
|
||||
|
||||
public static IBinder getBuiltInDisplay(int builtInDisplayId) {
|
||||
try {
|
||||
private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException {
|
||||
if (getBuiltInDisplayMethod == null) {
|
||||
// the method signature has changed in Android Q
|
||||
// <https://github.com/Genymobile/scrcpy/issues/586>
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId);
|
||||
getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class);
|
||||
} else {
|
||||
getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken");
|
||||
}
|
||||
return (IBinder) CLASS.getMethod("getPhysicalDisplayToken", long.class).invoke(null, builtInDisplayId);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return getBuiltInDisplayMethod;
|
||||
}
|
||||
|
||||
public static IBinder getBuiltInDisplay() {
|
||||
|
||||
try {
|
||||
Method method = getGetBuiltInDisplayMethod();
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// call getBuiltInDisplay(0)
|
||||
return (IBinder) method.invoke(null, 0);
|
||||
}
|
||||
|
||||
// call getInternalDisplayToken()
|
||||
return (IBinder) method.invoke(null);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
|
||||
if (setDisplayPowerModeMethod == null) {
|
||||
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
|
||||
}
|
||||
return setDisplayPowerModeMethod;
|
||||
}
|
||||
|
||||
public static void setDisplayPowerMode(IBinder displayToken, int mode) {
|
||||
try {
|
||||
CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
Method method = getSetDisplayPowerModeMethod();
|
||||
method.invoke(null, displayToken, mode);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,27 +1,95 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.os.IInterface;
|
||||
import android.view.IRotationWatcher;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class WindowManager {
|
||||
private final IInterface manager;
|
||||
private Method getRotationMethod;
|
||||
private Method freezeRotationMethod;
|
||||
private Method isRotationFrozenMethod;
|
||||
private Method thawRotationMethod;
|
||||
|
||||
public WindowManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
public int getRotation() {
|
||||
try {
|
||||
private Method getGetRotationMethod() throws NoSuchMethodException {
|
||||
if (getRotationMethod == null) {
|
||||
Class<?> cls = manager.getClass();
|
||||
try {
|
||||
return (Integer) manager.getClass().getMethod("getRotation").invoke(manager);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// method changed since this commit:
|
||||
// https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2
|
||||
return (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(manager);
|
||||
getRotationMethod = cls.getMethod("getDefaultDisplayRotation");
|
||||
} catch (NoSuchMethodException e) {
|
||||
// old version
|
||||
getRotationMethod = cls.getMethod("getRotation");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return getRotationMethod;
|
||||
}
|
||||
|
||||
private Method getFreezeRotationMethod() throws NoSuchMethodException {
|
||||
if (freezeRotationMethod == null) {
|
||||
freezeRotationMethod = manager.getClass().getMethod("freezeRotation", int.class);
|
||||
}
|
||||
return freezeRotationMethod;
|
||||
}
|
||||
|
||||
private Method getIsRotationFrozenMethod() throws NoSuchMethodException {
|
||||
if (isRotationFrozenMethod == null) {
|
||||
isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen");
|
||||
}
|
||||
return isRotationFrozenMethod;
|
||||
}
|
||||
|
||||
private Method getThawRotationMethod() throws NoSuchMethodException {
|
||||
if (thawRotationMethod == null) {
|
||||
thawRotationMethod = manager.getClass().getMethod("thawRotation");
|
||||
}
|
||||
return thawRotationMethod;
|
||||
}
|
||||
|
||||
public int getRotation() {
|
||||
try {
|
||||
Method method = getGetRotationMethod();
|
||||
return (int) method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void freezeRotation(int rotation) {
|
||||
try {
|
||||
Method method = getFreezeRotationMethod();
|
||||
method.invoke(manager, rotation);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRotationFrozen() {
|
||||
try {
|
||||
Method method = getIsRotationFrozenMethod();
|
||||
return (boolean) method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void thawRotation() {
|
||||
try {
|
||||
Method method = getThawRotationMethod();
|
||||
method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,11 +97,12 @@ public final class WindowManager {
|
|||
try {
|
||||
Class<?> cls = manager.getClass();
|
||||
try {
|
||||
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// display parameter added since this commit:
|
||||
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
|
||||
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// old version
|
||||
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
|
|
BIN
third_party/scrcpy-server.jar
vendored
BIN
third_party/scrcpy-server.jar
vendored
Binary file not shown.
Loading…
Add table
Reference in a new issue