diff --git a/.gitignore b/.gitignore index 674ee6f..705f29c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,5 @@ /server/gradlew /server/gradlew.bat /server/local.properties -build +build-* *.DS_Store diff --git a/QtScrcpy/QtScrcpy.pro b/QtScrcpy/QtScrcpy.pro index 12afc9a..41e46bc 100644 --- a/QtScrcpy/QtScrcpy.pro +++ b/QtScrcpy/QtScrcpy.pro @@ -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} diff --git a/QtScrcpy/device/controller/controller.cpp b/QtScrcpy/device/controller/controller.cpp index 5940385..5da8336 100644 --- a/QtScrcpy/device/controller/controller.cpp +++ b/QtScrcpy/device/controller/controller.cpp @@ -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); } diff --git a/QtScrcpy/device/controller/inputconvert/controlmsg.cpp b/QtScrcpy/device/controller/inputconvert/controlmsg.cpp index 45147ff..5b6dd9c 100644 --- a/QtScrcpy/device/controller/inputconvert/controlmsg.cpp +++ b/QtScrcpy/device/controller/inputconvert/controlmsg.cpp @@ -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); diff --git a/QtScrcpy/device/controller/inputconvert/controlmsg.h b/QtScrcpy/device/controller/inputconvert/controlmsg.h index e226428..5a2a547 100644 --- a/QtScrcpy/device/controller/inputconvert/controlmsg.h +++ b/QtScrcpy/device/controller/inputconvert/controlmsg.h @@ -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; diff --git a/QtScrcpy/device/controller/inputconvert/inputconvertgame.cpp b/QtScrcpy/device/controller/inputconvert/inputconvertgame.cpp index 6c30759..e4a03b5 100644 --- a/QtScrcpy/device/controller/inputconvert/inputconvertgame.cpp +++ b/QtScrcpy/device/controller/inputconvert/inputconvertgame.cpp @@ -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); } diff --git a/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp b/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp index 906774c..a971669 100644 --- a/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp +++ b/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp @@ -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); } diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index 5567f45..ff05964 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -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; diff --git a/QtScrcpy/device/device.h b/QtScrcpy/device/device.h index 4fff0d0..7e6dd0b 100644 --- a/QtScrcpy/device/device.h +++ b/QtScrcpy/device/device.h @@ -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; // 是否显示画面(或者仅仅后台录制) diff --git a/QtScrcpy/device/server/server.cpp b/QtScrcpy/device/server/server.cpp index d3a98f6..cdc53c5 100644 --- a/QtScrcpy/device/server/server.cpp +++ b/QtScrcpy/device/server/server.cpp @@ -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 << "-"; diff --git a/QtScrcpy/device/server/server.h b/QtScrcpy/device/server/server.h index d7ef1f1..d3e5f36 100644 --- a/QtScrcpy/device/server/server.h +++ b/QtScrcpy/device/server/server.h @@ -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; // 安卓端是否接收键鼠控制 diff --git a/QtScrcpy/dialog.cpp b/QtScrcpy/dialog.cpp index c8b6e69..8807f8e 100644 --- a/QtScrcpy/dialog.cpp +++ b/QtScrcpy/dialog.cpp @@ -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(); diff --git a/QtScrcpy/util/bufferutil.cpp b/QtScrcpy/util/bufferutil.cpp index 0c9362e..0627b75 100644 --- a/QtScrcpy/util/bufferutil.cpp +++ b/QtScrcpy/util/bufferutil.cpp @@ -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); diff --git a/QtScrcpy/util/bufferutil.h b/QtScrcpy/util/bufferutil.h index 39152ac..fb307c4 100644 --- a/QtScrcpy/util/bufferutil.h +++ b/QtScrcpy/util/bufferutil.h @@ -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); diff --git a/README_zh.md b/README_zh.md index 057c54e..ff156dc 100644 --- a/README_zh.md +++ b/README_zh.md @@ -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即可 diff --git a/server/build.gradle b/server/build.gradle index 3dec37c..6670520 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -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 { diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 0a8fe7c..195b04b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -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() { diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 2d1d96f..726b565 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -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); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 490ce05..dc0fa67 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -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 pointerProperties = new Vector(); - private Vector pointerCoords = new Vector(); + 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) { diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index e375cf5..a725d83 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -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 diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index f67a5a3..9448098 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -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) { diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index 846bc43..bbf4dd2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -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); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index 6b11a51..e2a3a1a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -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; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index d991419..26f13a5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -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; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 697cc96..5b993f3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -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; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Point.java b/server/src/main/java/com/genymobile/scrcpy/Point.java new file mode 100644 index 0000000..c2a30fa --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Point.java @@ -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 + '}'; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Pointer.java b/server/src/main/java/com/genymobile/scrcpy/Pointer.java new file mode 100644 index 0000000..b89cc25 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Pointer.java @@ -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; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/PointersState.java b/server/src/main/java/com/genymobile/scrcpy/PointersState.java new file mode 100644 index 0000000..d8daaff --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/PointersState.java @@ -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 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); + } + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/Position.java index e00a635..b46d2f7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/Position.java @@ -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 + '}'; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 4c0fb92..c9a37f8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -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; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 1c76afd..56b738f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -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); } }); diff --git a/server/src/main/java/com/genymobile/scrcpy/Size.java b/server/src/main/java/com/genymobile/scrcpy/Size.java index 0d546bb..fd4b697 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/Size.java @@ -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 + '}'; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java new file mode 100644 index 0000000..b1b8190 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -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()" + // + // + // 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" + // + 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()); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index a058a8b..592bdf6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -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); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 1fc78c2..44fa613 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -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; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index a730d1b..8ff074b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -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; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 543cabb..6f8941b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -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); } } -} \ No newline at end of file +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 5b5586f..227bbc8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -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 // 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); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 56330f9..cc687cd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -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);