diff --git a/QtScrcpy/QtScrcpy.pro b/QtScrcpy/QtScrcpy.pro index c2ef405..c947acc 100644 --- a/QtScrcpy/QtScrcpy.pro +++ b/QtScrcpy/QtScrcpy.pro @@ -32,6 +32,11 @@ msvc{ *g++*: QMAKE_CXXFLAGS += -Werror *msvc*: QMAKE_CXXFLAGS += /WX /wd4566 +# run a server debugger and wait for a client to be attached +# DEFINES += SERVER_DEBUGGER +# select the debugger method ('old' for Android < 9, 'new' for Android >= 9) +# DEFINES += SERVER_DEBUGGER_METHOD_NEW + # 源码 SOURCES += \ main.cpp \ diff --git a/QtScrcpy/device/controller/controller.cpp b/QtScrcpy/device/controller/controller.cpp index 4f45854..d24c69f 100644 --- a/QtScrcpy/device/controller/controller.cpp +++ b/QtScrcpy/device/controller/controller.cpp @@ -135,7 +135,7 @@ void Controller::onSetDeviceClipboard() if (!controlMsg) { return; } - controlMsg->setSetClipboardMsgData(text); + controlMsg->setSetClipboardMsgData(text, true); postControlMsg(controlMsg); } diff --git a/QtScrcpy/device/controller/inputconvert/controlmsg.cpp b/QtScrcpy/device/controller/inputconvert/controlmsg.cpp index 021b097..27e56eb 100644 --- a/QtScrcpy/device/controller/inputconvert/controlmsg.cpp +++ b/QtScrcpy/device/controller/inputconvert/controlmsg.cpp @@ -29,9 +29,9 @@ void ControlMsg::setInjectKeycodeMsgData(AndroidKeyeventAction action, AndroidKe void ControlMsg::setInjectTextMsgData(QString &text) { // write length (2 byte) + string (non nul-terminated) - if (CONTROL_MSG_TEXT_MAX_LENGTH < text.length()) { + if (CONTROL_MSG_INJECT_TEXT_MAX_LENGTH < text.length()) { // injecting a text takes time, so limit the text length - text = text.left(CONTROL_MSG_TEXT_MAX_LENGTH); + text = text.left(CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); } QByteArray tmp = text.toUtf8(); m_data.injectText.text = new char[tmp.length() + 1]; @@ -55,7 +55,7 @@ void ControlMsg::setInjectScrollMsgData(QRect position, qint32 hScroll, qint32 v m_data.injectScroll.vScroll = vScroll; } -void ControlMsg::setSetClipboardMsgData(QString &text) +void ControlMsg::setSetClipboardMsgData(QString &text, bool paste) { if (text.isEmpty()) { return; @@ -68,6 +68,7 @@ void ControlMsg::setSetClipboardMsgData(QString &text) m_data.setClipboard.text = new char[tmp.length() + 1]; memcpy(m_data.setClipboard.text, tmp.data(), tmp.length()); m_data.setClipboard.text[tmp.length()] = '\0'; + m_data.setClipboard.paste = paste; } void ControlMsg::setSetScreenPowerModeData(ControlMsg::ScreenPowerMode mode) @@ -124,6 +125,7 @@ QByteArray ControlMsg::serializeData() BufferUtil::write32(buffer, m_data.injectScroll.vScroll); break; case CMT_SET_CLIPBOARD: + buffer.putChar(!!m_data.setClipboard.paste); BufferUtil::write16(buffer, static_cast(strlen(m_data.setClipboard.text))); buffer.write(m_data.setClipboard.text, strlen(m_data.setClipboard.text)); break; diff --git a/QtScrcpy/device/controller/inputconvert/controlmsg.h b/QtScrcpy/device/controller/inputconvert/controlmsg.h index 73f4669..380247d 100644 --- a/QtScrcpy/device/controller/inputconvert/controlmsg.h +++ b/QtScrcpy/device/controller/inputconvert/controlmsg.h @@ -9,8 +9,8 @@ #include "keycodes.h" #include "qscrcpyevent.h" -#define CONTROL_MSG_TEXT_MAX_LENGTH 300 -#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093 +#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 +#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4092 #define POINTER_ID_MOUSE static_cast(-1) // ControlMsg class ControlMsg : public QScrcpyEvent @@ -48,7 +48,7 @@ public: // position action动作对应的位置 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 setSetClipboardMsgData(QString &text, bool paste); void setSetScreenPowerModeData(ControlMsg::ScreenPowerMode mode); QByteArray serializeData(); @@ -90,6 +90,7 @@ private: struct { char *text = Q_NULLPTR; + bool paste = true; } setClipboard; struct { diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index bbc5c3b..4e19d00 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -303,6 +303,8 @@ void Device::startServer() params.crop = "-"; params.control = true; params.useReverse = m_params.useReverse; + params.lockVideoOrientation = m_params.lockVideoOrientation; + params.stayAwake = m_params.stayAwake; m_server->start(params); }); } diff --git a/QtScrcpy/device/device.h b/QtScrcpy/device/device.h index de8b109..f574493 100644 --- a/QtScrcpy/device/device.h +++ b/QtScrcpy/device/device.h @@ -35,6 +35,8 @@ public: bool display = true; // 是否显示画面(或者仅仅后台录制) QString gameScript = ""; // 游戏映射脚本 bool renderExpiredFrames = false; // 是否渲染延迟视频帧 + int lockVideoOrientation = -1; // 是否锁定视频方向 + int stayAwake = false; // 是否保持唤醒 }; enum GroupControlState { diff --git a/QtScrcpy/device/server/server.cpp b/QtScrcpy/device/server/server.cpp index 2886eba..ec51093 100644 --- a/QtScrcpy/device/server/server.cpp +++ b/QtScrcpy/device/server/server.cpp @@ -124,12 +124,29 @@ bool Server::execute() args << "shell"; args << QString("CLASSPATH=%1").arg(Config::getInstance().getServerPath()); args << "app_process"; - args << "/"; // unused; + +#ifdef SERVER_DEBUGGER +#define SERVER_DEBUGGER_PORT "5005" + + args << +#ifdef SERVER_DEBUGGER_METHOD_NEW + /* Android 9 and above */ + "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,server=y,address=" +#else + /* Android 8 and below */ + "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" +#endif + SERVER_DEBUGGER_PORT, +#endif + + args << "/"; // unused; args << "com.genymobile.scrcpy.Server"; args << Config::getInstance().getServerVersion(); + args << Config::getInstance().getLogLevel(); args << QString::number(m_params.maxSize); args << QString::number(m_params.bitRate); args << QString::number(m_params.maxFps); + args << QString::number(m_params.lockVideoOrientation); args << (m_tunnelForward ? "true" : "false"); if (m_params.crop.isEmpty()) { args << "-"; @@ -138,6 +155,24 @@ bool Server::execute() } args << "true"; // always send frame meta (packet boundaries + timestamp) args << (m_params.control ? "true" : "false"); + args << "0"; // display id + args << "false"; // show touch + args << (m_params.stayAwake ? "true" : "false"); // stay awake + // code option + // https://github.com/Genymobile/scrcpy/commit/080a4ee3654a9b7e96c8ffe37474b5c21c02852a + // + args << "-"; + +#ifdef SERVER_DEBUGGER + qInfo("Server debugger waiting for a client on device port " SERVER_DEBUGGER_PORT "..."); + // From the computer, run + // adb forward tcp:5005 tcp:5005 + // Then, from Android Studio: Run > Debug > Edit configurations... + // On the left, click on '+', "Remote", with: + // Host: localhost + // Port: 5005 + // Then click on "Debug" +#endif // adb -s P7C0218510000537 shell CLASSPATH=/data/local/tmp/scrcpy-server app_process / com.genymobile.scrcpy.Server 0 8000000 false // mark: crop input format: "width:height:x:y" or - for no crop, for example: "100:200:0:0" diff --git a/QtScrcpy/device/server/server.h b/QtScrcpy/device/server/server.h index d45a573..496398b 100644 --- a/QtScrcpy/device/server/server.h +++ b/QtScrcpy/device/server/server.h @@ -26,14 +26,16 @@ class Server : public QObject public: struct ServerParams { - QString serial = ""; // 设备序列号 - quint16 localPort = 27183; // reverse时本地监听端口 - quint16 maxSize = 720; // 视频分辨率 - quint32 bitRate = 8000000; // 视频比特率 - quint32 maxFps = 60; // 视频最大帧率 - QString crop = "-"; // 视频裁剪 - bool control = true; // 安卓端是否接收键鼠控制 - bool useReverse = true; // true:先使用adb reverse,失败后自动使用adb forward;false:直接使用adb forward + QString serial = ""; // 设备序列号 + quint16 localPort = 27183; // reverse时本地监听端口 + quint16 maxSize = 720; // 视频分辨率 + quint32 bitRate = 8000000; // 视频比特率 + quint32 maxFps = 60; // 视频最大帧率 + QString crop = "-"; // 视频裁剪 + bool control = true; // 安卓端是否接收键鼠控制 + bool useReverse = true; // true:先使用adb reverse,失败后自动使用adb forward;false:直接使用adb forward + int lockVideoOrientation = -1; // 是否锁定视频方向 + int stayAwake = false; // 是否保持唤醒 }; explicit Server(QObject *parent = nullptr); diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index a833cd7..001d037 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -400,6 +400,11 @@ void VideoForm::updateShowSize(const QSize &newSize) if (isFullScreen() && m_device) { emit m_device->switchFullScreen(); } + + if (isMaximized()) { + showNormal(); + } + if (m_skin) { QMargins m = getMargins(vertical); showSize.setWidth(showSize.width() + m.left() + m.right()); @@ -570,7 +575,9 @@ void VideoForm::mouseMoveEvent(QMouseEvent *event) void VideoForm::mouseDoubleClickEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && !m_videoWidget->geometry().contains(event->pos())) { - removeBlackRect(); + if (!isMaximized()) { + removeBlackRect(); + } } if (event->button() == Qt::RightButton && m_device) { diff --git a/QtScrcpy/dialog.cpp b/QtScrcpy/dialog.cpp index 3a716f5..2417e64 100644 --- a/QtScrcpy/dialog.cpp +++ b/QtScrcpy/dialog.cpp @@ -109,6 +109,13 @@ void Dialog::initUI() ui->formatBox->addItem("mkv"); ui->formatBox->setCurrentIndex(Config::getInstance().getRecordFormatIndex()); + ui->lockOrientationBox->addItem(tr("no lock")); + ui->lockOrientationBox->addItem("0"); + ui->lockOrientationBox->addItem("90"); + ui->lockOrientationBox->addItem("180"); + ui->lockOrientationBox->addItem("270"); + ui->lockOrientationBox->setCurrentIndex(0); + ui->recordPathEdt->setText(Config::getInstance().getRecordPath()); ui->framelessCheck->setChecked(Config::getInstance().getFramelessWindow()); @@ -186,6 +193,8 @@ void Dialog::on_startServerBtn_clicked() params.useReverse = ui->useReverseCheck->isChecked(); params.display = !ui->notDisplayCheck->isChecked(); params.renderExpiredFrames = Config::getInstance().getRenderExpiredFrames(); + params.lockVideoOrientation = ui->lockOrientationBox->currentIndex() - 1; + params.stayAwake = ui->stayAwakeCheck->isChecked(); m_deviceManage.connectDevice(params); diff --git a/QtScrcpy/dialog.ui b/QtScrcpy/dialog.ui index 05c7eef..ae80cab 100644 --- a/QtScrcpy/dialog.ui +++ b/QtScrcpy/dialog.ui @@ -7,7 +7,7 @@ 0 0 420 - 492 + 517 @@ -106,6 +106,47 @@ + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + lock orientation: + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + @@ -290,6 +331,13 @@ + + + + stay awake + + + diff --git a/QtScrcpy/main.cpp b/QtScrcpy/main.cpp index 1fe6794..2638961 100644 --- a/QtScrcpy/main.cpp +++ b/QtScrcpy/main.cpp @@ -17,6 +17,9 @@ static QtMessageHandler g_oldMessageHandler = Q_NULLPTR; void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg); void installTranslator(); +static QtMsgType g_msgType = QtInfoMsg; +QtMsgType covertLogLevel(const QString &logLevel); + int main(int argc, char *argv[]) { // set env @@ -38,6 +41,8 @@ int main(int argc, char *argv[]) qputenv("QTSCRCPY_KEYMAP_PATH", "../../../keymap"); #endif + g_msgType = covertLogLevel(Config::getInstance().getLogLevel()); + // set on QApplication before int opengl = Config::getInstance().getDesktopOpenGL(); if (0 == opengl) { @@ -136,17 +141,53 @@ void installTranslator() qApp->installTranslator(&translator); } +QtMsgType covertLogLevel(const QString &logLevel) +{ + if ("debug" == logLevel) { + return QtDebugMsg; + } + + if ("info" == logLevel) { + return QtInfoMsg; + } + + if ("warn" == logLevel) { + return QtWarningMsg; + } + + if ("error" == logLevel) { + return QtCriticalMsg; + } + +#ifdef QT_NO_DEBUG + return QtInfoMsg; +#else + return QtDebugMsg; +#endif +} + void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { if (g_oldMessageHandler) { g_oldMessageHandler(type, context, msg); } - if (QtDebugMsg < type) { + // qt log info big than warning? + float fLogLevel = 1.0f * g_msgType; + if (QtInfoMsg == g_msgType) { + fLogLevel = QtDebugMsg + 0.5f; + } + float fLogLevel2 = 1.0f * type; + if (QtInfoMsg == type) { + fLogLevel2 = QtDebugMsg + 0.5f; + } + + if (fLogLevel <= fLogLevel2) { if (g_mainDlg && g_mainDlg->isVisible() && !g_mainDlg->filterLog(msg)) { g_mainDlg->outLog(msg); } } + if (QtFatalMsg == type) { //abort(); } diff --git a/QtScrcpy/res/i18n/QtScrcpy_en.qm b/QtScrcpy/res/i18n/QtScrcpy_en.qm index 62f5dce..9022ff7 100644 Binary files a/QtScrcpy/res/i18n/QtScrcpy_en.qm and b/QtScrcpy/res/i18n/QtScrcpy_en.qm differ diff --git a/QtScrcpy/res/i18n/QtScrcpy_en.ts b/QtScrcpy/res/i18n/QtScrcpy_en.ts index 4b9df55..9c131b5 100644 --- a/QtScrcpy/res/i18n/QtScrcpy_en.ts +++ b/QtScrcpy/res/i18n/QtScrcpy_en.ts @@ -49,17 +49,17 @@ Dialog - + Wireless Wireless - + wireless connect wireless connect - + wireless disconnect wireless disconnect @@ -69,13 +69,13 @@ Start Config - + record save path: record save path: - - + + select path select path @@ -85,47 +85,57 @@ record format: - + record screen record screen - + frameless frameless - + + lock orientation: + lock orientation: + + + show fps show fps - + + stay awake + stay awake + + + stop all server stop all server - + adb command: adb command: - + terminate terminate - + execute execute - + clear clear - + reverse connection reverse connection @@ -134,17 +144,17 @@ auto enable - + background record background record - + screen-off screen-off - + apply apply @@ -154,37 +164,37 @@ max size: - + always on top always on top - + refresh script refresh script - + get device IP get device IP - + USB line USB line - + stop server stop server - + start server start server - + device serial: device serial: @@ -198,20 +208,25 @@ bit rate: - + start adbd start adbd - + refresh devices refresh devices - + original original + + + no lock + no lock + QObject @@ -226,7 +241,7 @@ You can download it at the following address: This software is completely open source and free.\nStrictly used for illegal purposes, or at your own risk.\nYou can download it at the following address: - + This software is completely open source and free. Strictly used for illegal purposes, or at your own risk. You can download it at the following address: This software is completely open source and free. Strictly used for illegal purposes, or at your own risk. You can download it at the following address: @@ -322,7 +337,7 @@ You can download it at the following address: file transfer failed - + file does not exist file does not exist diff --git a/QtScrcpy/res/i18n/QtScrcpy_zh.qm b/QtScrcpy/res/i18n/QtScrcpy_zh.qm index 2a6e5c8..3c6f972 100644 Binary files a/QtScrcpy/res/i18n/QtScrcpy_zh.qm and b/QtScrcpy/res/i18n/QtScrcpy_zh.qm differ diff --git a/QtScrcpy/res/i18n/QtScrcpy_zh.ts b/QtScrcpy/res/i18n/QtScrcpy_zh.ts index bb0223f..27ea074 100644 --- a/QtScrcpy/res/i18n/QtScrcpy_zh.ts +++ b/QtScrcpy/res/i18n/QtScrcpy_zh.ts @@ -49,17 +49,17 @@ Dialog - + Wireless 无线 - + wireless connect 无线连接 - + wireless disconnect 无线断开 @@ -69,13 +69,13 @@ 启动配置 - + record save path: 录像保存路径: - - + + select path 选择路径 @@ -85,47 +85,57 @@ 录制格式: - + record screen 录制屏幕 - + frameless 无边框 - + + lock orientation: + 锁定方向: + + + show fps 显示fps - + + stay awake + 保持唤醒 + + + stop all server 停止所有服务 - + adb command: adb命令: - + terminate 终止 - + execute 执行 - + clear 清理 - + reverse connection 反向连接 @@ -134,17 +144,17 @@ 自动启用脚本 - + background record 后台录制 - + screen-off 自动息屏 - + apply 应用脚本 @@ -154,37 +164,37 @@ 最大尺寸: - + always on top 窗口置顶 - + refresh script 刷新脚本 - + get device IP 获取设备IP - + USB line USB线 - + stop server 停止服务 - + start server 启动服务 - + device serial: 设备序列号: @@ -198,20 +208,25 @@ 比特率: - + start adbd 启动adbd - + refresh devices 刷新设备列表 - + original 原始 + + + no lock + 不锁定 + QObject @@ -226,7 +241,7 @@ You can download it at the following address: 本软件完全开源免费.\n严禁用于非法用途,否则后果自负.\n你可以在下面地址下载: - + This software is completely open source and free. Strictly used for illegal purposes, or at your own risk. You can download it at the following address: 本软件完全开源免费,严禁用于非法用途,否则后果自负,你可以在下面地址下载: @@ -322,7 +337,7 @@ You can download it at the following address: 文件传输失败 - + file does not exist 文件不存在 diff --git a/QtScrcpy/util/config.cpp b/QtScrcpy/util/config.cpp index bf9c43a..e363bc4 100644 --- a/QtScrcpy/util/config.cpp +++ b/QtScrcpy/util/config.cpp @@ -34,6 +34,9 @@ #define COMMON_ADB_PATH_KEY "AdbPath" #define COMMON_ADB_PATH_DEF "" +#define COMMON_LOG_LEVEL_KEY "LogLevel" +#define COMMON_LOG_LEVEL_DEF "info" + // user data #define COMMON_RECORD_KEY "RecordPath" #define COMMON_RECORD_DEF "" @@ -74,7 +77,7 @@ Config &Config::getInstance() static Config config; return config; } - +#include const QString &Config::getConfigPath() { if (s_configPath.isEmpty()) { @@ -265,6 +268,15 @@ QString Config::getAdbPath() return adbPath; } +QString Config::getLogLevel() +{ + QString logLevel; + m_settings->beginGroup(GROUP_COMMON); + logLevel = m_settings->value(COMMON_LOG_LEVEL_KEY, COMMON_LOG_LEVEL_DEF).toString(); + m_settings->endGroup(); + return logLevel; +} + QString Config::getTitle() { QString title; diff --git a/QtScrcpy/util/config.h b/QtScrcpy/util/config.h index 274c9e5..8f82fda 100644 --- a/QtScrcpy/util/config.h +++ b/QtScrcpy/util/config.h @@ -21,6 +21,7 @@ public: QString getPushFilePath(); QString getServerPath(); QString getAdbPath(); + QString getLogLevel(); // user data QString getRecordPath(); diff --git a/config/config.ini b/config/config.ini index 9a36955..f72d577 100644 --- a/config/config.ini +++ b/config/config.ini @@ -10,8 +10,11 @@ RenderExpiredFrames=0 # 视频解码方式:-1 自动,0 软解,1 dx硬解,2 opengl硬解 UseDesktopOpenGL=-1 # scrcpy-server的版本号(不要修改) -ServerVersion=1.12.1 +ServerVersion=1.14 # scrcpy-server推送到安卓设备的路径 ServerPath=/data/local/tmp/scrcpy-server.jar # 自定义adb路径,例如D:/android/tools/adb.exe AdbPath= + +# Set the log level (debug, info, warn, error) +LogLevel=info diff --git a/docs/DEVELOP.md b/docs/DEVELOP.md index 92c3ce8..4d8acc5 100644 --- a/docs/DEVELOP.md +++ b/docs/DEVELOP.md @@ -189,7 +189,7 @@ The client uses 4 threads: recording, - the **controller** thread, sending _control messages_ to the server, - the **receiver** thread (managed by the controller), receiving _device - messages_ from the client. + messages_ from the server. In addition, another thread can be started if necessary to handle APK installation or file push requests (via drag&drop on the main window) or to @@ -214,7 +214,7 @@ When a new decoded frame is available, the decoder _swaps_ the decoding and rendering frame (with proper synchronization). Thus, it immediatly starts to decode a new frame while the main thread renders the last one. -If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw +If a [recorder] is present (i.e. `--record` is enabled), then it muxes the raw H.264 packet to the output video file. [stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h @@ -282,6 +282,15 @@ meson x -Dserver_debugger=true meson configure x -Dserver_debugger=true ``` +If your device runs Android 8 or below, set the `server_debugger_method` to +`old` in addition: + +```bash +meson x -Dserver_debugger=true -Dserver_debugger_method=old +# or, if x is already configured +meson configure x -Dserver_debugger=true -Dserver_debugger_method=old +``` + Then recompile. When you start scrcpy, it will start a debugger on port 5005 on the device. diff --git a/docs/FAQ.md b/docs/FAQ.md index 7ea6efa..74284f1 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -3,6 +3,26 @@ 如果在此文档没有解决你的问题,描述你的问题,截图软件控制台中打印的日志,一起发到QQ群里提问。 +# adb问题 +## ADB版本之间的冲突 +``` +adb server version (41) doesn't match this client (39); killing... +``` +当你的电脑中运行不同版本的adb时,会发生此错误。你必须保证所有程序使用相同版本的adb。 +现在你有两个办法解决这个问题: +1. 任务管理器找到adb进程并杀死 +2. 配置QtScrcpy的config.ini中的AdbPath路径指向当前使用的adb + +## 手机通过数据线连接电脑,刷新设备列表以后,没有任何设备出现 +随便下载一个手机助手,尝试连接成功以后,再用QtScrcpy刷新设备列表连接 + +# 控制问题 +## 可以看到画面,但无法控制 +有些手机(小米等手机)需要额外打开控制权限,检查是否USB调试里打开了允许模拟点击 + +![image](image/USB调试(安全设置).jpg) + +# 其它 ## 支持声音(软件不做支持) [关于转发安卓声音到PC的讨论](https://github.com/Genymobile/scrcpy/issues/14#issuecomment-543204526) @@ -21,19 +41,11 @@ QtScrcpy.exe>属性>兼容性>更改高DPI设置>覆盖高DPI缩放行为>由以 ## 无法输入中文 手机端安装搜狗输入法/QQ输入法就可以支持输入中文了 -## 可以看到画面,但无法控制 -有些手机(小米等手机)需要额外打开控制权限,检查是否USB调试里打开了允许模拟点击 - -![image](image/USB调试(安全设置).jpg) - ## 可以控制,但无法看到画面 控制台错误信息可能会包含 QOpenGLShaderProgram::attributeLocation(vertexIn): shader program is not linked 一般是由于显卡不支持当前的视频渲染方式,config.ini里修改下解码方式,改成1或者2试试 -## 手机通过数据线连接电脑,刷新设备列表以后,没有任何设备出现 -随便下载一个手机助手,尝试连接成功以后,再用QtScrcpy刷新设备列表连接 - ## 错误信息:AdbProcess::error:adb server version (40) doesnt match this client (41) 任务管理找到adb进程并杀死,重新操作即可 diff --git a/docs/TODO.md b/docs/TODO.md index ef510a9..4d3ca81 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,21 +1,22 @@ -最后同步scrcpy 31bd95022bc525be42ca273d59a3211d964d278b +最后同步scrcpy 3c0fc8f54f42bf6e7eca35b352a7d343749b65c4 # TODO ## 低优先级 -- [单独线程统计帧率](https://github.com/Genymobile/scrcpy/commit/e2a272bf99ecf48fcb050177113f903b3fb323c4) - text转换 https://github.com/Genymobile/scrcpy/commit/c916af0984f72a60301d13fa8ef9a85112f54202?tdsourcetag=s_pctim_aiomsg +- 关闭number lock时的数字小键盘处理 https://github.com/Genymobile/scrcpy/commit/cd69eb4a4fecf8167208399def4ef536b59c9d22 +- mipmapping https://github.com/Genymobile/scrcpy/commit/bea7658807d276aeab7d18d856a366c83ee05827 ## 中优先级 - 脚本 - 某些机器软解不行 - opengles 3.0 兼容性参考[这里](https://github.com/libretro/glsl-shaders/blob/master/nnedi3/shaders/yuv-to-rgb-2x.glsl) +- 通过host:track-devices实现自动连接 https://www.jianshu.com/p/2cb86c6de76c +- 旋转 https://github.com/Genymobile/scrcpy/commit/d48b375a1dbc8bed92e3424b5967e59c2d8f6ca1 ## 高优先级 - linux打包以及版本号 - 关于 -- 旋转 -- ubuntu自动打包 -- 版本号抽离优化 +- 音频转发 https://github.com/rom1v/sndcpy # mark ## ffmpeg diff --git a/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl b/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl new file mode 100644 index 0000000..46d7f7c --- /dev/null +++ b/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2008, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +/** + * {@hide} + */ +oneway interface IOnPrimaryClipChangedListener { + void dispatchPrimaryClipChanged(); +} diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java new file mode 100644 index 0000000..7455563 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -0,0 +1,77 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.ContentProvider; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import java.io.File; +import java.io.IOException; + +/** + * Handle the cleanup of scrcpy, even if the main process is killed. + *

+ * This is useful to restore some state when scrcpy is closed, even on device disconnection (which kills the scrcpy process). + */ +public final class CleanUp { + + public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; + + private CleanUp() { + // not instantiable + } + + public static void configure(boolean disableShowTouches, int restoreStayOn) throws IOException { + boolean needProcess = disableShowTouches || restoreStayOn != -1; + if (needProcess) { + startProcess(disableShowTouches, restoreStayOn); + } else { + // There is no additional clean up to do when scrcpy dies + unlinkSelf(); + } + } + + private static void startProcess(boolean disableShowTouches, int restoreStayOn) throws IOException { + String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(restoreStayOn)}; + + ProcessBuilder builder = new ProcessBuilder(cmd); + builder.environment().put("CLASSPATH", SERVER_PATH); + builder.start(); + } + + private static void unlinkSelf() { + try { + new File(SERVER_PATH).delete(); + } catch (Exception e) { + Ln.e("Could not unlink server", e); + } + } + + public static void main(String... args) { + unlinkSelf(); + + try { + // Wait for the server to die + System.in.read(); + } catch (IOException e) { + // Expected when the server is dead + } + + Ln.i("Cleaning up"); + + boolean disableShowTouches = Boolean.parseBoolean(args[0]); + int restoreStayOn = Integer.parseInt(args[1]); + + if (disableShowTouches || restoreStayOn != -1) { + ServiceManager serviceManager = new ServiceManager(); + try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) { + if (disableShowTouches) { + Ln.i("Disabling \"show touches\""); + settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0"); + } + if (restoreStayOn != -1) { + Ln.i("Restoring \"stay awake\""); + settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn)); + } + } + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java new file mode 100644 index 0000000..1897bda --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java @@ -0,0 +1,112 @@ +package com.genymobile.scrcpy; + +import java.util.ArrayList; +import java.util.List; + +public class CodecOption { + private String key; + private Object value; + + public CodecOption(String key, Object value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public Object getValue() { + return value; + } + + public static List parse(String codecOptions) { + if ("-".equals(codecOptions)) { + return null; + } + + List result = new ArrayList<>(); + + boolean escape = false; + StringBuilder buf = new StringBuilder(); + + for (char c : codecOptions.toCharArray()) { + switch (c) { + case '\\': + if (escape) { + buf.append('\\'); + escape = false; + } else { + escape = true; + } + break; + case ',': + if (escape) { + buf.append(','); + escape = false; + } else { + // This comma is a separator between codec options + String codecOption = buf.toString(); + result.add(parseOption(codecOption)); + // Clear buf + buf.setLength(0); + } + break; + default: + buf.append(c); + break; + } + } + + if (buf.length() > 0) { + String codecOption = buf.toString(); + result.add(parseOption(codecOption)); + } + + return result; + } + + private static CodecOption parseOption(String option) { + int equalSignIndex = option.indexOf('='); + if (equalSignIndex == -1) { + throw new IllegalArgumentException("'=' expected"); + } + String keyAndType = option.substring(0, equalSignIndex); + if (keyAndType.length() == 0) { + throw new IllegalArgumentException("Key may not be null"); + } + + String key; + String type; + + int colonIndex = keyAndType.indexOf(':'); + if (colonIndex != -1) { + key = keyAndType.substring(0, colonIndex); + type = keyAndType.substring(colonIndex + 1); + } else { + key = keyAndType; + type = "int"; // assume int by default + } + + Object value; + String valueString = option.substring(equalSignIndex + 1); + switch (type) { + case "int": + value = Integer.parseInt(valueString); + break; + case "long": + value = Long.parseLong(valueString); + break; + case "float": + value = Float.parseFloat(valueString); + break; + case "string": + value = valueString; + break; + default: + throw new IllegalArgumentException("Invalid codec option type (int, long, float, str): " + type); + } + + return new CodecOption(key, value); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 195b04b..7d0ab7a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -17,6 +17,8 @@ public final class ControlMessage { public static final int TYPE_SET_SCREEN_POWER_MODE = 9; public static final int TYPE_ROTATE_DEVICE = 10; + public static final int FLAGS_PASTE = 1; + private int type; private String text; private int metaState; // KeyEvent.META_* @@ -28,6 +30,7 @@ public final class ControlMessage { private Position position; private int hScroll; private int vScroll; + private int flags; private ControlMessage() { } @@ -68,10 +71,13 @@ public final class ControlMessage { return msg; } - public static ControlMessage createSetClipboard(String text) { + public static ControlMessage createSetClipboard(String text, boolean paste) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_CLIPBOARD; msg.text = text; + if (paste) { + msg.flags = FLAGS_PASTE; + } return msg; } @@ -134,4 +140,8 @@ public final class ControlMessage { public int getVScroll() { return vScroll; } + + public int getFlags() { + return flags; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 726b565..fbf49a6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -8,15 +8,16 @@ import java.nio.charset.StandardCharsets; public class ControlMessageReader { - private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; - 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; + static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; + static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; + static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; + static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; + static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; - public static final int TEXT_MAX_LENGTH = 300; - public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; - private static final int RAW_BUFFER_SIZE = 1024; + public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4092; // 4096 - 1 (type) - 1 (parse flag) - 2 (length) + public static final int INJECT_TEXT_MAX_LENGTH = 300; + + private static final int RAW_BUFFER_SIZE = 4096; private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); @@ -122,7 +123,6 @@ public class ControlMessageReader { return ControlMessage.createInjectText(text); } - @SuppressWarnings("checkstyle:MagicNumber") private ControlMessage parseInjectTouchEvent() { if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { return null; @@ -149,11 +149,15 @@ public class ControlMessageReader { } private ControlMessage parseSetClipboard() { + if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { + return null; + } + boolean parse = buffer.get() != 0; String text = parseString(); if (text == null) { return null; } - return ControlMessage.createSetClipboard(text); + return ControlMessage.createSetClipboard(text, parse); } private ControlMessage parseSetScreenPowerMode() { @@ -172,12 +176,10 @@ public class ControlMessageReader { return new Position(x, y, screenWidth, screenHeight); } - @SuppressWarnings("checkstyle:MagicNumber") private static int toUnsigned(short value) { return value & 0xffff; } - @SuppressWarnings("checkstyle:MagicNumber") private static int toUnsigned(byte value) { return value & 0xff; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index dc0fa67..960c6a6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -1,10 +1,8 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.InputManager; - +import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; -import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; @@ -47,11 +45,10 @@ public class Controller { } } - @SuppressWarnings("checkstyle:MagicNumber") public void control() throws IOException { // on start, power on the device if (!device.isScreenOn()) { - injectKeycode(KeyEvent.KEYCODE_POWER); + device.injectKeycode(KeyEvent.KEYCODE_POWER); // dirty hack // After POWER is injected, the device is powered on asynchronously. @@ -76,19 +73,29 @@ public class Controller { ControlMessage msg = connection.receiveControlMessage(); switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: - injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + if (device.supportsInputEvents()) { + injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + } break; case ControlMessage.TYPE_INJECT_TEXT: - injectText(msg.getText()); + if (device.supportsInputEvents()) { + injectText(msg.getText()); + } break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + if (device.supportsInputEvents()) { + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + } break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + if (device.supportsInputEvents()) { + injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + } break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - pressBackOrTurnScreenOn(); + if (device.supportsInputEvents()) { + pressBackOrTurnScreenOn(); + } break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: device.expandNotificationPanel(); @@ -98,13 +105,22 @@ public class Controller { break; case ControlMessage.TYPE_GET_CLIPBOARD: String clipboardText = device.getClipboardText(); - sender.pushClipboardText(clipboardText); + if (clipboardText != null) { + sender.pushClipboardText(clipboardText); + } break; case ControlMessage.TYPE_SET_CLIPBOARD: - device.setClipboardText(msg.getText()); + boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0; + setClipboard(msg.getText(), paste); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: - device.setScreenPowerMode(msg.getAction()); + if (device.supportsInputEvents()) { + int mode = msg.getAction(); + boolean setPowerModeOk = device.setScreenPowerMode(mode); + if (setPowerModeOk) { + Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + } + } break; case ControlMessage.TYPE_ROTATE_DEVICE: device.rotateDevice(); @@ -115,7 +131,7 @@ public class Controller { } private boolean injectKeycode(int action, int keycode, int metaState) { - return injectKeyEvent(action, keycode, 0, metaState); + return device.injectKeyEvent(action, keycode, 0, metaState); } private boolean injectChar(char c) { @@ -126,7 +142,7 @@ public class Controller { return false; } for (KeyEvent event : events) { - if (!injectEvent(event)) { + if (!device.injectEvent(event)) { return false; } } @@ -182,7 +198,7 @@ public class Controller { 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); + return device.injectEvent(event); } private boolean injectScroll(Position position, int hScroll, int vScroll) { @@ -204,27 +220,26 @@ public class Controller { 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); - } - - private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { - long now = SystemClock.uptimeMillis(); - KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, - InputDevice.SOURCE_KEYBOARD); - return injectEvent(event); - } - - private boolean injectKeycode(int keyCode) { - return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); - } - - private boolean injectEvent(InputEvent event) { - return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + InputDevice.SOURCE_TOUCHSCREEN, 0); + return device.injectEvent(event); } private boolean pressBackOrTurnScreenOn() { int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; - return injectKeycode(keycode); + return device.injectKeycode(keycode); + } + + private boolean setClipboard(String text, boolean paste) { + boolean ok = device.setClipboardText(text); + if (ok) { + Ln.i("Device clipboard set"); + } + + // On Android >= 7, also press the PASTE key if requested + if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + device.injectKeycode(KeyEvent.KEYCODE_PASTE); + } + + return ok; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index a725d83..0ec4304 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -84,7 +84,6 @@ public final class DesktopConnection implements Closeable { controlSocket.close(); } - @SuppressWarnings("checkstyle:MagicNumber") private void send(String deviceName, int width, int height) throws IOException { byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4]; diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 9448098..349486c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,15 +1,23 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.ContentProvider; +import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; +import android.content.IOnPrimaryClipChangedListener; import android.graphics.Rect; import android.os.Build; import android.os.IBinder; -import android.os.RemoteException; +import android.os.SystemClock; import android.view.IRotationWatcher; +import android.view.InputDevice; import android.view.InputEvent; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; + +import java.util.concurrent.atomic.AtomicBoolean; public final class Device { @@ -20,18 +28,47 @@ public final class Device { void onRotationChanged(int rotation); } + public interface ClipboardListener { + void onClipboardTextChanged(String text); + } + private final ServiceManager serviceManager = new ServiceManager(); private ScreenInfo screenInfo; private RotationListener rotationListener; + private ClipboardListener clipboardListener; + private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); + + /** + * Logical display identifier + */ + private final int displayId; + + /** + * The surface flinger layer stack associated with this logical display + */ + private final int layerStack; + + private final boolean supportsInputEvents; public Device(Options options) { - screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize()); - registerRotationWatcher(new IRotationWatcher.Stub() { + displayId = options.getDisplayId(); + DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + int[] displayIds = serviceManager.getDisplayManager().getDisplayIds(); + throw new InvalidDisplayIdException(displayId, displayIds); + } + + int displayInfoFlags = displayInfo.getFlags(); + + screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); + layerStack = displayInfo.getLayerStack(); + + serviceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { @Override - public void onRotationChanged(int rotation) throws RemoteException { + public void onRotationChanged(int rotation) { synchronized (Device.this) { - screenInfo = screenInfo.withRotation(rotation); + screenInfo = screenInfo.withDeviceRotation(rotation); // notify if (rotationListener != null) { @@ -39,104 +76,120 @@ public final class Device { } } } - }); + }, displayId); + + if (options.getControl()) { + // If control is enabled, synchronize Android clipboard to the computer automatically + serviceManager.getClipboardManager().addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { + @Override + public void dispatchPrimaryClipChanged() { + if (isSettingClipboard.get()) { + // This is a notification for the change we are currently applying, ignore it + return; + } + synchronized (Device.this) { + if (clipboardListener != null) { + String text = getClipboardText(); + if (text != null) { + clipboardListener.onClipboardTextChanged(text); + } + } + } + } + }); + } + + if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { + Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); + } + + // main display or any display on Android >= Q + supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + if (!supportsInputEvents) { + Ln.w("Input events are not supported for secondary displays before Android 10"); + } } public synchronized ScreenInfo getScreenInfo() { return screenInfo; } - private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { - DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); - boolean rotated = (displayInfo.getRotation() & 1) != 0; - Size deviceSize = displayInfo.getSize(); - Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); - if (crop != null) { - if (rotated) { - // the crop (provided by the user) is expressed in the natural orientation - crop = flipRect(crop); - } - if (!contentRect.intersect(crop)) { - // intersect() changes contentRect so that it is intersected with crop - Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); - contentRect = new Rect(); // empty - } - } - - Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); - return new ScreenInfo(contentRect, videoSize, rotated); - } - - private static String formatCrop(Rect rect) { - return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; - } - - @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; - // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) - w &= ~7; // in case it's not a multiple of 8 - h &= ~7; - if (maxSize > 0) { - if (BuildConfig.DEBUG && maxSize % 8 != 0) { - throw new AssertionError("Max size must be a multiple of 8"); - } - boolean portrait = h > w; - int major = portrait ? h : w; - int minor = portrait ? w : h; - if (major > maxSize) { - int minorExact = minor * maxSize / major; - // +4 to round the value to the nearest multiple of 8 - minor = (minorExact + 4) & ~7; - major = maxSize; - } - w = portrait ? minor : major; - h = portrait ? major : minor; - } - return new Size(w, h); + public int getLayerStack() { + return layerStack; } public Point getPhysicalPoint(Position position) { // it hides the field on purpose, to read it with a lock @SuppressWarnings("checkstyle:HiddenField") ScreenInfo screenInfo = getScreenInfo(); // read with synchronization - Size videoSize = screenInfo.getVideoSize(); - Size clientVideoSize = position.getScreenSize(); - if (!videoSize.equals(clientVideoSize)) { + + // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation + Size unlockedVideoSize = screenInfo.getUnlockedVideoSize(); + + int reverseVideoRotation = screenInfo.getReverseVideoRotation(); + // reverse the video rotation to apply the events + Position devicePosition = position.rotate(reverseVideoRotation); + + Size clientVideoSize = devicePosition.getScreenSize(); + if (!unlockedVideoSize.equals(clientVideoSize)) { // The client sends a click relative to a video with wrong dimensions, // the device may have been rotated since the event was generated, so ignore the event return null; } Rect contentRect = screenInfo.getContentRect(); - Point point = position.getPoint(); - 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); + Point point = devicePosition.getPoint(); + int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth(); + int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight(); + return new Point(convertedX, convertedY); } public static String getDeviceName() { return Build.MODEL; } - public boolean injectInputEvent(InputEvent inputEvent, int mode) { + public boolean supportsInputEvents() { + return supportsInputEvents; + } + + public boolean injectEvent(InputEvent inputEvent, int mode) { + if (!supportsInputEvents()) { + throw new AssertionError("Could not inject input event if !supportsInputEvents()"); + } + + if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) { + return false; + } + return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); } + public boolean injectEvent(InputEvent event) { + return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + } + + public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { + long now = SystemClock.uptimeMillis(); + KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, + InputDevice.SOURCE_KEYBOARD); + return injectEvent(event); + } + + public boolean injectKeycode(int keyCode) { + return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); + } + public boolean isScreenOn() { return serviceManager.getPowerManager().isScreenOn(); } - public void registerRotationWatcher(IRotationWatcher rotationWatcher) { - serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher); - } - public synchronized void setRotationListener(RotationListener rotationListener) { this.rotationListener = rotationListener; } + public synchronized void setClipboardListener(ClipboardListener clipboardListener) { + this.clipboardListener = clipboardListener; + } + public void expandNotificationPanel() { serviceManager.getStatusBarManager().expandNotificationsPanel(); } @@ -153,22 +206,23 @@ public final class Device { return s.toString(); } - public void setClipboardText(String text) { - serviceManager.getClipboardManager().setText(text); - Ln.i("Device clipboard set"); + public boolean setClipboardText(String text) { + isSettingClipboard.set(true); + boolean ok = serviceManager.getClipboardManager().setText(text); + isSettingClipboard.set(false); + return ok; } /** * @param mode one of the {@code SCREEN_POWER_MODE_*} constants */ - public void setScreenPowerMode(int mode) { + public boolean setScreenPowerMode(int mode) { IBinder d = SurfaceControl.getBuiltInDisplay(); if (d == null) { Ln.e("Could not get built-in display"); - return; + return false; } - SurfaceControl.setDisplayPowerMode(d, mode); - Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + return SurfaceControl.setDisplayPowerMode(d, mode); } /** @@ -192,7 +246,7 @@ public final class Device { } } - static Rect flipRect(Rect crop) { - return new Rect(crop.top, crop.left, crop.bottom, crop.right); + public ContentProvider createSettingsProvider() { + return serviceManager.getActivityManager().createSettingsProvider(); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index e2a3a1a..6c7f363 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -13,7 +13,6 @@ public class DeviceMessageWriter { private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); - @SuppressWarnings("checkstyle:MagicNumber") public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { buffer.clear(); buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD); diff --git a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java index 639869b..4b8036f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java @@ -1,12 +1,24 @@ package com.genymobile.scrcpy; public final class DisplayInfo { + private final int displayId; private final Size size; private final int rotation; + private final int layerStack; + private final int flags; - public DisplayInfo(Size size, int rotation) { + public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; + + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) { + this.displayId = displayId; this.size = size; this.rotation = rotation; + this.layerStack = layerStack; + this.flags = flags; + } + + public int getDisplayId() { + return displayId; } public Size getSize() { @@ -16,5 +28,13 @@ public final class DisplayInfo { public int getRotation() { return rotation; } + + public int getLayerStack() { + return layerStack; + } + + public int getFlags() { + return flags; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java new file mode 100644 index 0000000..81e3b90 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy; + +public class InvalidDisplayIdException extends RuntimeException { + + private final int displayId; + private final int[] availableDisplayIds; + + public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) { + super("There is no display having id " + displayId); + this.displayId = displayId; + this.availableDisplayIds = availableDisplayIds; + } + + public int getDisplayId() { + return displayId; + } + + public int[] getAvailableDisplayIds() { + return availableDisplayIds; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index 26f13a5..c218fa0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -15,14 +15,25 @@ public final class Ln { DEBUG, INFO, WARN, ERROR } - private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO; + private static Level threshold = Level.INFO; private Ln() { // not instantiable } + /** + * Initialize the log level. + *

+ * Must be called before starting any new thread. + * + * @param level the log level + */ + public static void initLogLevel(Level level) { + threshold = level; + } + public static boolean isEnabled(Level level) { - return level.ordinal() >= THRESHOLD.ordinal(); + return level.ordinal() >= threshold.ordinal(); } public static void d(String message) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 5b993f3..06312a3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -3,13 +3,27 @@ package com.genymobile.scrcpy; import android.graphics.Rect; public class Options { + private Ln.Level logLevel; private int maxSize; private int bitRate; private int maxFps; + private int lockedVideoOrientation; private boolean tunnelForward; private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean control; + private int displayId; + private boolean showTouches; + private boolean stayAwake; + private String codecOptions; + + public Ln.Level getLogLevel() { + return logLevel; + } + + public void setLogLevel(Ln.Level logLevel) { + this.logLevel = logLevel; + } public int getMaxSize() { return maxSize; @@ -35,6 +49,14 @@ public class Options { this.maxFps = maxFps; } + public int getLockedVideoOrientation() { + return lockedVideoOrientation; + } + + public void setLockedVideoOrientation(int lockedVideoOrientation) { + this.lockedVideoOrientation = lockedVideoOrientation; + } + public boolean isTunnelForward() { return tunnelForward; } @@ -66,4 +88,36 @@ public class Options { public void setControl(boolean control) { this.control = control; } + + public int getDisplayId() { + return displayId; + } + + public void setDisplayId(int displayId) { + this.displayId = displayId; + } + + public boolean getShowTouches() { + return showTouches; + } + + public void setShowTouches(boolean showTouches) { + this.showTouches = showTouches; + } + + public boolean getStayAwake() { + return stayAwake; + } + + public void setStayAwake(boolean stayAwake) { + this.stayAwake = stayAwake; + } + + public String getCodecOptions() { + return codecOptions; + } + + public void setCodecOptions(String codecOptions) { + this.codecOptions = codecOptions; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/Position.java index b46d2f7..e9b6d8a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/Position.java @@ -23,6 +23,19 @@ public class Position { return screenSize; } + public Position rotate(int rotation) { + switch (rotation) { + case 1: + return new Position(new Point(screenSize.getHeight() - point.getY(), point.getX()), screenSize.rotate()); + case 2: + return new Position(new Point(screenSize.getWidth() - point.getX(), screenSize.getHeight() - point.getY()), screenSize); + case 3: + return new Position(new Point(point.getY(), screenSize.getWidth() - point.getX()), screenSize.rotate()); + default: + return this; + } + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index c9a37f8..d722388 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -6,40 +6,37 @@ 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; import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; public class ScreenEncoder implements Device.RotationListener { 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 String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder"; private static final int NO_PTS = -1; private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); + private List codecOptions; private int bitRate; private int maxFps; - private int iFrameInterval; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; - this.iFrameInterval = iFrameInterval; - } - - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { - this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); + this.codecOptions = codecOptions; } @Override @@ -53,21 +50,40 @@ public class ScreenEncoder implements Device.RotationListener { public void streamScreen(Device device, FileDescriptor fd) throws IOException { Workarounds.prepareMainLooper(); - Workarounds.fillAppInfo(); - MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval); + try { + internalStreamScreen(device, fd); + } catch (NullPointerException e) { + // Retry with workarounds enabled: + // + // + Ln.d("Applying workarounds to avoid NullPointerException"); + Workarounds.fillAppInfo(); + internalStreamScreen(device, fd); + } + } + + private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { + MediaFormat format = createFormat(bitRate, maxFps, codecOptions); device.setRotationListener(this); boolean alive; try { do { MediaCodec codec = createCodec(); IBinder display = createDisplay(); - Rect contentRect = device.getScreenInfo().getContentRect(); - Rect videoRect = device.getScreenInfo().getVideoSize().toRect(); + ScreenInfo screenInfo = device.getScreenInfo(); + Rect contentRect = screenInfo.getContentRect(); + // include the locked video orientation + Rect videoRect = screenInfo.getVideoSize().toRect(); + // does not include the locked video orientation + Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); + int videoRotation = screenInfo.getVideoRotation(); + int layerStack = device.getLayerStack(); + setSize(format, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); - setDisplaySurface(display, surface, contentRect, videoRect); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); codec.start(); try { alive = encode(codec, fd); @@ -135,27 +151,49 @@ public class ScreenEncoder implements Device.RotationListener { } private static MediaCodec createCodec() throws IOException { - return MediaCodec.createEncoderByType("video/avc"); + return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); } - @SuppressWarnings("checkstyle:MagicNumber") - private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) { + private static void setCodecOption(MediaFormat format, CodecOption codecOption) { + String key = codecOption.getKey(); + Object value = codecOption.getValue(); + + if (value instanceof Integer) { + format.setInteger(key, (Integer) value); + } else if (value instanceof Long) { + format.setLong(key, (Long) value); + } else if (value instanceof Float) { + format.setFloat(key, (Float) value); + } else if (value instanceof String) { + format.setString(key, (String) value); + } + + Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); + } + + private static MediaFormat createFormat(int bitRate, int maxFps, List codecOptions) { MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, "video/avc"); + format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); // 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); + format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL); // display the very first frame, and recover from bad quality when no new frames 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"); + // The key existed privately before Android 10: + // + // + format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); + } + + if (codecOptions != null) { + for (CodecOption option : codecOptions) { + setCodecOption(format, option); } } + return format; } @@ -172,12 +210,12 @@ public class ScreenEncoder implements Device.RotationListener { format.setInteger(MediaFormat.KEY_HEIGHT, height); } - private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) { + private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { SurfaceControl.openTransaction(); try { SurfaceControl.setDisplaySurface(display, surface); - SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect); - SurfaceControl.setDisplayLayerStack(display, 0); + SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); + SurfaceControl.setDisplayLayerStack(display, layerStack); } finally { SurfaceControl.closeTransaction(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index f2fce1d..10acfb5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -3,29 +3,161 @@ package com.genymobile.scrcpy; import android.graphics.Rect; public final class ScreenInfo { + /** + * Device (physical) size, possibly cropped + */ private final Rect contentRect; // device size, possibly cropped - private final Size videoSize; - private final boolean rotated; - public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) { + /** + * Video size, possibly smaller than the device size, already taking the device rotation and crop into account. + *

+ * However, it does not include the locked video orientation. + */ + private final Size unlockedVideoSize; + + /** + * Device rotation, related to the natural device orientation (0, 1, 2 or 3) + */ + private final int deviceRotation; + + /** + * The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW) + */ + private final int lockedVideoOrientation; + + public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) { this.contentRect = contentRect; - this.videoSize = videoSize; - this.rotated = rotated; + this.unlockedVideoSize = unlockedVideoSize; + this.deviceRotation = deviceRotation; + this.lockedVideoOrientation = lockedVideoOrientation; } public Rect getContentRect() { return contentRect; } - public Size getVideoSize() { - return videoSize; + /** + * Return the video size as if locked video orientation was not set. + * + * @return the unlocked video size + */ + public Size getUnlockedVideoSize() { + return unlockedVideoSize; } - public ScreenInfo withRotation(int rotation) { - boolean newRotated = (rotation & 1) != 0; - if (rotated == newRotated) { + /** + * Return the actual video size if locked video orientation is set. + * + * @return the actual video size + */ + public Size getVideoSize() { + if (getVideoRotation() % 2 == 0) { + return unlockedVideoSize; + } + + return unlockedVideoSize.rotate(); + } + + public int getDeviceRotation() { + return deviceRotation; + } + + public ScreenInfo withDeviceRotation(int newDeviceRotation) { + if (newDeviceRotation == deviceRotation) { return this; } - return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated); + // true if changed between portrait and landscape + boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0; + Rect newContentRect; + Size newUnlockedVideoSize; + if (orientationChanged) { + newContentRect = flipRect(contentRect); + newUnlockedVideoSize = unlockedVideoSize.rotate(); + } else { + newContentRect = contentRect; + newUnlockedVideoSize = unlockedVideoSize; + } + return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation); + } + + public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) { + int rotation = displayInfo.getRotation(); + Size deviceSize = displayInfo.getSize(); + Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); + if (crop != null) { + if (rotation % 2 != 0) { // 180s preserve dimensions + // the crop (provided by the user) is expressed in the natural orientation + crop = flipRect(crop); + } + if (!contentRect.intersect(crop)) { + // intersect() changes contentRect so that it is intersected with crop + Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); + contentRect = new Rect(); // empty + } + } + + Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); + return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation); + } + + private static String formatCrop(Rect rect) { + return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; + } + + 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; + // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) + w &= ~7; // in case it's not a multiple of 8 + h &= ~7; + if (maxSize > 0) { + if (BuildConfig.DEBUG && maxSize % 8 != 0) { + throw new AssertionError("Max size must be a multiple of 8"); + } + boolean portrait = h > w; + int major = portrait ? h : w; + int minor = portrait ? w : h; + if (major > maxSize) { + int minorExact = minor * maxSize / major; + // +4 to round the value to the nearest multiple of 8 + minor = (minorExact + 4) & ~7; + major = maxSize; + } + w = portrait ? minor : major; + h = portrait ? major : minor; + } + return new Size(w, h); + } + + private static Rect flipRect(Rect crop) { + return new Rect(crop.top, crop.left, crop.bottom, crop.right); + } + + /** + * Return the rotation to apply to the device rotation to get the requested locked video orientation + * + * @return the rotation offset + */ + public int getVideoRotation() { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (deviceRotation + 4 - lockedVideoOrientation) % 4; + } + + /** + * Return the rotation to apply to the requested locked video orientation to get the device rotation + * + * @return the (reverse) rotation offset + */ + public int getReverseVideoRotation() { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (lockedVideoOrientation + 4 - deviceRotation) % 4; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 56b738f..44b3afd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,32 +1,74 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.ContentProvider; + import android.graphics.Rect; import android.media.MediaCodec; +import android.os.BatteryManager; import android.os.Build; -import java.io.File; import java.io.IOException; +import java.util.List; +import java.util.Locale; public final class Server { - private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; private Server() { // not instantiable } private static void scrcpy(Options options) throws IOException { + Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); final Device device = new Device(options); + List codecOptions = CodecOption.parse(options.getCodecOptions()); + + boolean mustDisableShowTouchesOnCleanUp = false; + int restoreStayOn = -1; + if (options.getShowTouches() || options.getStayAwake()) { + try (ContentProvider settings = device.createSettingsProvider()) { + if (options.getShowTouches()) { + String oldValue = settings.getAndPutValue(ContentProvider.TABLE_SYSTEM, "show_touches", "1"); + // If "show touches" was disabled, it must be disabled back on clean up + mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); + } + + if (options.getStayAwake()) { + int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; + String oldValue = settings.getAndPutValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); + try { + restoreStayOn = Integer.parseInt(oldValue); + if (restoreStayOn == stayOn) { + // No need to restore + restoreStayOn = -1; + } + } catch (NumberFormatException e) { + restoreStayOn = 0; + } + } + } + } + + CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn); + boolean tunnelForward = options.isTunnelForward(); + try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { - ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); + ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions); if (options.getControl()) { - Controller controller = new Controller(device, connection); + final Controller controller = new Controller(device, connection); // asynchronous startController(controller); startDeviceMessageSender(controller.getSender()); + + device.setClipboardListener(new Device.ClipboardListener() { + @Override + public void onClipboardTextChanged(String text) { + controller.getSender().pushClipboardText(text); + } + }); } try { @@ -67,7 +109,6 @@ public final class Server { }).start(); } - @SuppressWarnings("checkstyle:MagicNumber") private static Options createOptions(String... args) { if (args.length < 1) { throw new IllegalArgumentException("Missing client version"); @@ -76,41 +117,59 @@ public final class Server { 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 + ")"); + "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - if (args.length != 8) { - throw new IllegalArgumentException("Expecting 8 parameters"); + final int expectedParameters = 14; + if (args.length != expectedParameters) { + throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); } Options options = new Options(); - int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8 + Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase(Locale.ENGLISH)); + options.setLogLevel(level); + + int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8 options.setMaxSize(maxSize); - int bitRate = Integer.parseInt(args[2]); + int bitRate = Integer.parseInt(args[3]); options.setBitRate(bitRate); - int maxFps = Integer.parseInt(args[3]); + int maxFps = Integer.parseInt(args[4]); options.setMaxFps(maxFps); + int lockedVideoOrientation = Integer.parseInt(args[5]); + options.setLockedVideoOrientation(lockedVideoOrientation); + // use "adb forward" instead of "adb tunnel"? (so the server must listen) - boolean tunnelForward = Boolean.parseBoolean(args[4]); + boolean tunnelForward = Boolean.parseBoolean(args[6]); options.setTunnelForward(tunnelForward); - Rect crop = parseCrop(args[5]); + Rect crop = parseCrop(args[7]); options.setCrop(crop); - boolean sendFrameMeta = Boolean.parseBoolean(args[6]); + boolean sendFrameMeta = Boolean.parseBoolean(args[8]); options.setSendFrameMeta(sendFrameMeta); - boolean control = Boolean.parseBoolean(args[7]); + boolean control = Boolean.parseBoolean(args[9]); options.setControl(control); + int displayId = Integer.parseInt(args[10]); + options.setDisplayId(displayId); + + boolean showTouches = Boolean.parseBoolean(args[11]); + options.setShowTouches(showTouches); + + boolean stayAwake = Boolean.parseBoolean(args[12]); + options.setStayAwake(stayAwake); + + String codecOptions = args[13]; + options.setCodecOptions(codecOptions); + return options; } - @SuppressWarnings("checkstyle:MagicNumber") private static Rect parseCrop(String crop) { if ("-".equals(crop)) { return null; @@ -127,15 +186,6 @@ public final class Server { return new Rect(x, y, x + width, y + height); } - private static void unlinkSelf() { - try { - new File(SERVER_PATH).delete(); - } catch (Exception 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) { @@ -147,6 +197,16 @@ public final class Server { } } } + if (e instanceof InvalidDisplayIdException) { + InvalidDisplayIdException idie = (InvalidDisplayIdException) e; + int[] displayIds = idie.getAvailableDisplayIds(); + if (displayIds != null && displayIds.length > 0) { + Ln.e("Try to use one of the available display ids:"); + for (int id : displayIds) { + Ln.e(" scrcpy --display " + id); + } + } + } } public static void main(String... args) throws Exception { @@ -158,8 +218,10 @@ public final class Server { } }); - unlinkSelf(); Options options = createOptions(args); + + Ln.initLogLevel(options.getLogLevel()); + scrcpy(options); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java index 199fc8c..dac0546 100644 --- a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java @@ -5,7 +5,6 @@ public final class StringUtils { // not instantiable } - @SuppressWarnings("checkstyle:MagicNumber") public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) { int len = utf8.length; if (len <= maxLength) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index b1b8190..351cc57 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -28,7 +28,7 @@ public final class Workarounds { Looper.prepareMainLooper(); } - @SuppressLint("PrivateApi") + @SuppressLint("PrivateApi,DiscouragedPrivateApi") public static void fillAppInfo() { try { // ActivityThread activityThread = new ActivityThread(); @@ -73,7 +73,7 @@ public final class Workarounds { 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()); + Ln.d("Could not fill app info: " + throwable.getMessage()); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java new file mode 100644 index 0000000..71967c5 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -0,0 +1,87 @@ +package com.genymobile.scrcpy.wrappers; + +import com.genymobile.scrcpy.Ln; + +import android.os.Binder; +import android.os.IBinder; +import android.os.IInterface; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ActivityManager { + + private final IInterface manager; + private Method getContentProviderExternalMethod; + private boolean getContentProviderExternalMethodLegacy; + private Method removeContentProviderExternalMethod; + + public ActivityManager(IInterface manager) { + this.manager = manager; + } + + private Method getGetContentProviderExternalMethod() throws NoSuchMethodException { + if (getContentProviderExternalMethod == null) { + try { + getContentProviderExternalMethod = manager.getClass() + .getMethod("getContentProviderExternal", String.class, int.class, IBinder.class, String.class); + } catch (NoSuchMethodException e) { + // old version + getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class); + getContentProviderExternalMethodLegacy = true; + } + } + return getContentProviderExternalMethod; + } + + private Method getRemoveContentProviderExternalMethod() throws NoSuchMethodException { + if (removeContentProviderExternalMethod == null) { + removeContentProviderExternalMethod = manager.getClass().getMethod("removeContentProviderExternal", String.class, IBinder.class); + } + return removeContentProviderExternalMethod; + } + + private ContentProvider getContentProviderExternal(String name, IBinder token) { + try { + Method method = getGetContentProviderExternalMethod(); + Object[] args; + if (!getContentProviderExternalMethodLegacy) { + // new version + args = new Object[]{name, ServiceManager.USER_ID, token, null}; + } else { + // old version + args = new Object[]{name, ServiceManager.USER_ID, token}; + } + // ContentProviderHolder providerHolder = getContentProviderExternal(...); + Object providerHolder = method.invoke(manager, args); + if (providerHolder == null) { + return null; + } + // IContentProvider provider = providerHolder.provider; + Field providerField = providerHolder.getClass().getDeclaredField("provider"); + providerField.setAccessible(true); + Object provider = providerField.get(providerHolder); + if (provider == null) { + return null; + } + return new ContentProvider(this, provider, name, token); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + void removeContentProviderExternal(String name, IBinder token) { + try { + Method method = getRemoveContentProviderExternalMethod(); + method.invoke(manager, name, token); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + } + } + + public ContentProvider createSettingsProvider() { + return getContentProviderExternal("settings", new Binder()); + } +} 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 592bdf6..e25b6e9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; import android.content.ClipData; +import android.content.IOnPrimaryClipChangedListener; import android.os.Build; import android.os.IInterface; @@ -10,13 +11,10 @@ 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 Method getPrimaryClipMethod; private Method setPrimaryClipMethod; + private Method addPrimaryClipChangedListener; public ClipboardManager(IInterface manager) { this.manager = manager; @@ -46,17 +44,17 @@ public class ClipboardManager { 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, ServiceManager.PACKAGE_NAME); } - return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID); + return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.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); + method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME); } else { - method.invoke(manager, clipData, PACKAGE_NAME, USER_ID); + method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } } @@ -74,13 +72,48 @@ public class ClipboardManager { } } - public void setText(CharSequence text) { + public boolean setText(CharSequence text) { try { Method method = getSetPrimaryClipMethod(); ClipData clipData = ClipData.newPlainText(null, text); setPrimaryClip(method, manager, clipData); + return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); + return false; + } + } + + private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener) + throws InvocationTargetException, IllegalAccessException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + method.invoke(manager, listener, ServiceManager.PACKAGE_NAME); + } else { + method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); + } + } + + private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException { + if (addPrimaryClipChangedListener == null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); + } else { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); + } + } + return addPrimaryClipChangedListener; + } + + public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { + try { + Method method = getAddPrimaryClipChangedListener(); + addPrimaryClipChangedListener(method, manager, listener); + return true; + } 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/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java new file mode 100644 index 0000000..b43494c --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -0,0 +1,132 @@ +package com.genymobile.scrcpy.wrappers; + +import com.genymobile.scrcpy.Ln; + +import android.os.Bundle; +import android.os.IBinder; + +import java.io.Closeable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ContentProvider implements Closeable { + + public static final String TABLE_SYSTEM = "system"; + public static final String TABLE_SECURE = "secure"; + public static final String TABLE_GLOBAL = "global"; + + // See android/providerHolder/Settings.java + private static final String CALL_METHOD_GET_SYSTEM = "GET_system"; + private static final String CALL_METHOD_GET_SECURE = "GET_secure"; + private static final String CALL_METHOD_GET_GLOBAL = "GET_global"; + + private static final String CALL_METHOD_PUT_SYSTEM = "PUT_system"; + private static final String CALL_METHOD_PUT_SECURE = "PUT_secure"; + private static final String CALL_METHOD_PUT_GLOBAL = "PUT_global"; + + private static final String CALL_METHOD_USER_KEY = "_user"; + + private static final String NAME_VALUE_TABLE_VALUE = "value"; + + private final ActivityManager manager; + // android.content.IContentProvider + private final Object provider; + private final String name; + private final IBinder token; + + private Method callMethod; + private boolean callMethodLegacy; + + ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) { + this.manager = manager; + this.provider = provider; + this.name = name; + this.token = token; + } + + private Method getCallMethod() throws NoSuchMethodException { + if (callMethod == null) { + try { + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class); + } catch (NoSuchMethodException e) { + // old version + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class); + callMethodLegacy = true; + } + } + return callMethod; + } + + private Bundle call(String callMethod, String arg, Bundle extras) { + try { + Method method = getCallMethod(); + Object[] args; + if (!callMethodLegacy) { + args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras}; + } else { + args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras}; + } + return (Bundle) method.invoke(provider, args); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + public void close() { + manager.removeContentProviderExternal(name, token); + } + + private static String getGetMethod(String table) { + switch (table) { + case TABLE_SECURE: + return CALL_METHOD_GET_SECURE; + case TABLE_SYSTEM: + return CALL_METHOD_GET_SYSTEM; + case TABLE_GLOBAL: + return CALL_METHOD_GET_GLOBAL; + default: + throw new IllegalArgumentException("Invalid table: " + table); + } + } + + private static String getPutMethod(String table) { + switch (table) { + case TABLE_SECURE: + return CALL_METHOD_PUT_SECURE; + case TABLE_SYSTEM: + return CALL_METHOD_PUT_SYSTEM; + case TABLE_GLOBAL: + return CALL_METHOD_PUT_GLOBAL; + default: + throw new IllegalArgumentException("Invalid table: " + table); + } + } + + public String getValue(String table, String key) { + String method = getGetMethod(table); + Bundle arg = new Bundle(); + arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID); + Bundle bundle = call(method, key, arg); + if (bundle == null) { + return null; + } + return bundle.getString("value"); + } + + public void putValue(String table, String key, String value) { + String method = getPutMethod(table); + Bundle arg = new Bundle(); + arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID); + arg.putString(NAME_VALUE_TABLE_VALUE, value); + call(method, key, arg); + } + + public String getAndPutValue(String table, String key, String value) { + String oldValue = getValue(table, key); + if (!value.equals(oldValue)) { + putValue(table, key, value); + } + return oldValue; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index 568afac..cedb3f4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -12,15 +12,28 @@ public final class DisplayManager { this.manager = manager; } - public DisplayInfo getDisplayInfo() { + public DisplayInfo getDisplayInfo(int displayId) { try { - Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); + Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); + if (displayInfo == null) { + return null; + } Class cls = displayInfo.getClass(); // width and height already take the rotation into account int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo); int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo); int rotation = cls.getDeclaredField("rotation").getInt(displayInfo); - return new DisplayInfo(new Size(width, height), rotation); + int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); + int flags = cls.getDeclaredField("flags").getInt(displayInfo); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public int[] getDisplayIds() { + try { + return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager); } catch (Exception e) { throw new AssertionError(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 44fa613..e17b5a1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -17,6 +17,8 @@ public final class InputManager { private final IInterface manager; private Method injectInputEventMethod; + private static Method setDisplayIdMethod; + public InputManager(IInterface manager) { this.manager = manager; } @@ -37,4 +39,22 @@ public final class InputManager { return false; } } + + private static Method getSetDisplayIdMethod() throws NoSuchMethodException { + if (setDisplayIdMethod == null) { + setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class); + } + return setDisplayIdMethod; + } + + public static boolean setDisplayId(InputEvent inputEvent, int displayId) { + try { + Method method = getSetDisplayIdMethod(); + method.invoke(inputEvent, displayId); + return true; + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Cannot associate a display id to the input event", e); + return false; + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index 0b625c9..c4ce59c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -6,8 +6,12 @@ import android.os.IInterface; import java.lang.reflect.Method; -@SuppressLint("PrivateApi") +@SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class ServiceManager { + + public static final String PACKAGE_NAME = "com.android.shell"; + public static final int USER_ID = 0; + private final Method getServiceMethod; private WindowManager windowManager; @@ -16,6 +20,7 @@ public final class ServiceManager { private PowerManager powerManager; private StatusBarManager statusBarManager; private ClipboardManager clipboardManager; + private ActivityManager activityManager; public ServiceManager() { try { @@ -76,4 +81,21 @@ public final class ServiceManager { } return clipboardManager; } + + public ActivityManager getActivityManager() { + if (activityManager == null) { + try { + // On old Android versions, the ActivityManager is not exposed via AIDL, + // so use ActivityManagerNative.getDefault() + Class cls = Class.forName("android.app.ActivityManagerNative"); + Method getDefaultMethod = cls.getDeclaredMethod("getDefault"); + IInterface am = (IInterface) getDefaultMethod.invoke(null); + activityManager = new ActivityManager(am); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + return activityManager; + } } 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 227bbc8..8fbb860 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -121,12 +121,14 @@ public final class SurfaceControl { return setDisplayPowerModeMethod; } - public static void setDisplayPowerMode(IBinder displayToken, int mode) { + public static boolean setDisplayPowerMode(IBinder displayToken, int mode) { try { Method method = getSetDisplayPowerModeMethod(); method.invoke(null, displayToken, mode); + return true; } 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/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index cc687cd..faa366a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -93,13 +93,13 @@ public final class WindowManager { } } - public void registerRotationWatcher(IRotationWatcher rotationWatcher) { + public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { try { Class cls = manager.getClass(); try { // 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); + cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId); } catch (NoSuchMethodException e) { // old version cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); diff --git a/third_party/scrcpy-server b/third_party/scrcpy-server index 640d0cf..73d292a 100644 Binary files a/third_party/scrcpy-server and b/third_party/scrcpy-server differ