From ddc9f222cb6e48cc4eacbb5120f93d2e62f192b2 Mon Sep 17 00:00:00 2001 From: RainbowXie <34147843+RainbowXie@users.noreply.github.com> Date: Mon, 23 Aug 2021 11:08:06 +0800 Subject: [PATCH 1/6] Qt api update Qt::MidButton': MidButton is deprecated. Use MiddleButton instead --- .../device/controller/inputconvert/inputconvertnormal.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp b/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp index 260604e..a19be58 100644 --- a/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp +++ b/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp @@ -132,7 +132,11 @@ AndroidMotioneventButtons InputConvertNormal::convertMouseButtons(Qt::MouseButto if (buttonState & Qt::RightButton) { buttons |= AMOTION_EVENT_BUTTON_SECONDARY; } +#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) + if (buttonState & Qt::MiddleButton) { +#else if (buttonState & Qt::MidButton) { +#endif buttons |= AMOTION_EVENT_BUTTON_TERTIARY; } if (buttonState & Qt::XButton1) { From 1e2de40dc815d62f82e3c7c2c010c49189711881 Mon Sep 17 00:00:00 2001 From: leiyu Date: Fri, 3 Dec 2021 14:24:31 +0800 Subject: [PATCH 2/6] chore: Adapter Qt5.11 --- QtScrcpy/adb/adbprocess.cpp | 10 +++- .../inputconvert/inputconvertnormal.cpp | 10 ++-- QtScrcpy/device/ui/videoform.cpp | 12 +++++ QtScrcpy/dialog.cpp | 46 +++++++++---------- QtScrcpy/dialog.ui | 3 -- 5 files changed, 51 insertions(+), 30 deletions(-) diff --git a/QtScrcpy/adb/adbprocess.cpp b/QtScrcpy/adb/adbprocess.cpp index ad14835..c0bc626 100644 --- a/QtScrcpy/adb/adbprocess.cpp +++ b/QtScrcpy/adb/adbprocess.cpp @@ -116,9 +116,17 @@ QStringList AdbProcess::getDevicesSerialFromStdOut() { // get devices serial by adb devices QStringList serials; +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) QStringList devicesInfoList = m_standardOutput.split(QRegExp("\r\n|\n"), Qt::SkipEmptyParts); +#else + QStringList devicesInfoList = m_standardOutput.split(QRegExp("\r\n|\n"), QString::SkipEmptyParts); +#endif for (QString deviceInfo : devicesInfoList) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) QStringList deviceInfos = deviceInfo.split(QRegExp("\t"), Qt::SkipEmptyParts); +#else + QStringList deviceInfos = deviceInfo.split(QRegExp("\t"), QString::SkipEmptyParts); +#endif if (2 == deviceInfos.count() && 0 == deviceInfos[1].compare("device")) { serials << deviceInfos[0]; } @@ -131,7 +139,7 @@ QString AdbProcess::getDeviceIPFromStdOut() QString ip = ""; #if 0 QString strIPExp = "inet [\\d.]*"; - QRegExp ipRegExp(strIPExp,Qt::CaseInsensitive); + QRegExp ipRegExp(strIPExp, Qt::CaseInsensitive); if (ipRegExp.indexIn(m_standardOutput) != -1) { ip = ipRegExp.cap(0); ip = ip.right(ip.size() - 5); diff --git a/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp b/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp index a19be58..d90862c 100644 --- a/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp +++ b/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp @@ -47,9 +47,9 @@ void InputConvertNormal::mouseEvent(const QMouseEvent *from, const QSize &frameS } controlMsg->setInjectTouchMsgData( static_cast(POINTER_ID_MOUSE), action, - convertMouseButtons(from->buttons()), - QRect(pos.toPoint(), frameSize), - AMOTION_EVENT_ACTION_DOWN == action? 1.0f : 0.0f); + convertMouseButtons(from->buttons()), + QRect(pos.toPoint(), frameSize), + AMOTION_EVENT_ACTION_DOWN == action ? 1.0f : 0.0f); sendControlMsg(controlMsg); } @@ -64,7 +64,11 @@ void InputConvertNormal::wheelEvent(const QWheelEvent *from, const QSize &frameS qint32 vScroll = from->angleDelta().y() == 0 ? 0 : from->angleDelta().y() / abs(from->angleDelta().y()) * 2; // pos +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) QPointF pos = from->position(); +#else + QPointF pos = from->posF(); +#endif // convert pos pos.setX(pos.x() * frameSize.width() / showSize.width()); pos.setY(pos.y() * frameSize.height() / showSize.height()); diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index adef612..07281a0 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -627,6 +627,7 @@ void VideoForm::mouseDoubleClickEvent(QMouseEvent *event) void VideoForm::wheelEvent(QWheelEvent *event) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) if (m_videoWidget->geometry().contains(event->position().toPoint())) { if (!m_device) { return; @@ -634,6 +635,17 @@ void VideoForm::wheelEvent(QWheelEvent *event) QPointF pos = m_videoWidget->mapFrom(this, event->position().toPoint()); QWheelEvent wheelEvent( pos, event->globalPosition(), event->pixelDelta(), event->angleDelta(), event->buttons(), event->modifiers(), event->phase(), event->inverted()); +#else + if (m_videoWidget->geometry().contains(event->pos())) { + if (!m_device) { + return; + } + QPointF pos = m_videoWidget->mapFrom(this, event->pos()); + + QWheelEvent wheelEvent( + pos, event->globalPosF(), event->pixelDelta(), event->angleDelta(), event->delta(), event->orientation(), + event->buttons(), event->modifiers(), event->phase(), event->source(), event->inverted()); +#endif emit m_device->wheelEvent(&wheelEvent, m_videoWidget->frameSize(), m_videoWidget->size()); } } diff --git a/QtScrcpy/dialog.cpp b/QtScrcpy/dialog.cpp index 8cfebad..009a65c 100644 --- a/QtScrcpy/dialog.cpp +++ b/QtScrcpy/dialog.cpp @@ -89,11 +89,11 @@ Dialog::Dialog(QWidget *parent) : QDialog(parent), ui(new Ui::Dialog) m_hideIcon->setContextMenu(m_menu); m_hideIcon->show(); connect(m_showWindow, &QAction::triggered, this, &Dialog::slotShow); - connect(m_quit, &QAction::triggered, this, [this](){ + connect(m_quit, &QAction::triggered, this, [this]() { m_hideIcon->hide(); qApp->quit(); }); - connect(m_hideIcon, &QSystemTrayIcon::activated,this,&Dialog::slotActivated); + connect(m_hideIcon, &QSystemTrayIcon::activated, this, &Dialog::slotActivated); } Dialog::~Dialog() @@ -152,9 +152,9 @@ void Dialog::updateBootConfig(bool toView) if (toView) { UserBootConfig config = Config::getInstance().getUserBootConfig(); - if(config.bitRate == 0) { + if (config.bitRate == 0) { ui->bitRateBox->setCurrentText("Mbps"); - } else if(config.bitRate % 1000000 == 0) { + } else if (config.bitRate % 1000000 == 0) { ui->bitRateEdit->setText(QString::number(config.bitRate / 1000000)); ui->bitRateBox->setCurrentText("Mbps"); } else { @@ -203,7 +203,11 @@ void Dialog::execAdbCmd() } QString cmd = ui->adbCommandEdt->text().trimmed(); outLog("adb " + cmd, false); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) m_adb.execute(ui->serialBox->currentText().trimmed(), cmd.split(" ", Qt::SkipEmptyParts)); +#else + m_adb.execute(ui->serialBox->currentText().trimmed(), cmd.split(" ", QString::SkipEmptyParts)); +#endif } void Dialog::delayMs(int ms) @@ -249,9 +253,9 @@ void Dialog::closeEvent(QCloseEvent *event) { this->hide(); m_hideIcon->showMessage(tr("Notice"), - tr("Hidden here!"), - QSystemTrayIcon::Information, - 3000); + tr("Hidden here!"), + QSystemTrayIcon::Information, + 3000); event->ignore(); } @@ -521,14 +525,13 @@ void Dialog::on_usbConnectBtn_clicked() on_startServerBtn_clicked(); } -int Dialog::findDeviceFromeSerialBox(bool wifi) { +int Dialog::findDeviceFromeSerialBox(bool wifi) +{ QRegExp regIP("\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\:([0-9]|[1-9]\\d|[1-9]\\d{2}|[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])\\b"); - for (int i = 0; i < ui->serialBox->count(); ++i) - { + for (int i = 0; i < ui->serialBox->count(); ++i) { bool isWifi = regIP.exactMatch(ui->serialBox->itemText(i)); bool found = wifi ? isWifi : !isWifi; - if(found) - { + if (found) { return i; } } @@ -582,8 +585,8 @@ void Dialog::on_connectedPhoneList_itemDoubleClicked(QListWidgetItem *item) void Dialog::on_updateNameBtn_clicked() { - if(ui->serialBox->count()!=0) { - if(ui->userNameEdt->text().isEmpty()) { + if (ui->serialBox->count() != 0) { + if (ui->userNameEdt->text().isEmpty()) { Config::getInstance().setNickName(ui->serialBox->currentText(), "Phone"); } else { Config::getInstance().setNickName(ui->serialBox->currentText(), ui->userNameEdt->text()); @@ -591,30 +594,27 @@ void Dialog::on_updateNameBtn_clicked() on_updateDevice_clicked(); - qDebug()<<"Update OK!"; + qDebug() << "Update OK!"; } else { - qWarning()<<"No device is connected!"; + qWarning() << "No device is connected!"; } } void Dialog::on_useSingleModeCheck_clicked() { - if(ui->useSingleModeCheck->isChecked()) - { + if (ui->useSingleModeCheck->isChecked()) { ui->configGroupBox->hide(); ui->adbGroupBox->hide(); ui->wirelessGroupBox->hide(); ui->usbGroupBox->hide(); - } - else - { + } else { ui->configGroupBox->show(); ui->adbGroupBox->show(); ui->wirelessGroupBox->show(); ui->usbGroupBox->show(); } - QTimer::singleShot(0, this, [this](){ + QTimer::singleShot(0, this, [this]() { resize(width(), layout()->sizeHint().height()); }); } @@ -627,5 +627,5 @@ void Dialog::on_serialBox_currentIndexChanged(const QString &arg1) quint32 Dialog::getBitRate() { return ui->bitRateEdit->text().trimmed().toUInt() * - (ui->bitRateBox->currentText() == QString("Mbps") ? 1000000 : 1000); + (ui->bitRateBox->currentText() == QString("Mbps") ? 1000000 : 1000); } diff --git a/QtScrcpy/dialog.ui b/QtScrcpy/dialog.ui index 6968d5c..d99bafe 100644 --- a/QtScrcpy/dialog.ui +++ b/QtScrcpy/dialog.ui @@ -207,9 +207,6 @@ Mbps - - - Mbps From 0ebd4dbb6bd00690d23f8fb1b0fecb1b731cfea3 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Tue, 21 Dec 2021 19:02:40 +0800 Subject: [PATCH 3/6] bump scrcpy-server to 1.21 --- .github/workflows/macos.yml | 2 +- QtScrcpy/device/device.cpp | 4 ++-- QtScrcpy/device/server/server.cpp | 32 +++++++++++++++--------------- QtScrcpy/device/server/server.h | 2 +- QtScrcpy/util/config.cpp | 2 +- config/config.ini | 6 +++--- third_party/scrcpy-server | Bin 34930 -> 40067 bytes 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 105ba56..ba7a815 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -13,7 +13,7 @@ on: jobs: build: name: Build - runs-on: macos-latest + runs-on: macos-10.15 strategy: matrix: qt-ver: [5.15.1] diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index 7c833a9..98901d6 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -300,14 +300,14 @@ void Device::startServer() // support wireless connect, example: //m_server->start("192.168.0.174:5555", 27183, m_maxSize, m_bitRate, ""); // only one devices, serial can be null - // mark: crop input format: "width:height:x:y" or - for no crop, for example: "100:200:0:0" + // mark: crop input format: "width:height:x:y" or "" for no crop, for example: "100:200:0:0" Server::ServerParams params; params.serial = m_params.serial; params.localPort = m_params.localPort; params.maxSize = m_params.maxSize; params.bitRate = m_params.bitRate; params.maxFps = m_params.maxFps; - params.crop = "-"; + params.crop = ""; params.control = true; params.useReverse = m_params.useReverse; params.lockVideoOrientation = m_params.lockVideoOrientation; diff --git a/QtScrcpy/device/server/server.cpp b/QtScrcpy/device/server/server.cpp index 5616622..756da4b 100644 --- a/QtScrcpy/device/server/server.cpp +++ b/QtScrcpy/device/server/server.cpp @@ -142,27 +142,27 @@ bool Server::execute() 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"); + args << QString("log_level=%1").arg(Config::getInstance().getLogLevel()); + args << QString("max_size=%1").arg(QString::number(m_params.maxSize)); + args << QString("bit_rate=%1").arg(QString::number(m_params.bitRate)); + args << QString("max_fps=%1").arg(QString::number(m_params.maxFps)); + args << QString("lock_video_orientation=%1").arg(QString::number(m_params.lockVideoOrientation)); + args << QString("tunnel_forward=%1").arg((m_tunnelForward ? "true" : "false")); if (m_params.crop.isEmpty()) { - args << "-"; + args << "crop="; } else { - args << m_params.crop; + args << QString("crop=%1").arg(m_params.crop); } - 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 + args << "send_frame_meta=true"; // always send frame meta (packet boundaries + timestamp) + args << QString("control=%1").arg((m_params.control ? "true" : "false")); + args << "display_id=0"; // display id + args << "show_touches=false"; // show touch + args << QString("stay_awake=%1").arg((m_params.stayAwake ? "true" : "false")); // stay awake // code option // https://github.com/Genymobile/scrcpy/commit/080a4ee3654a9b7e96c8ffe37474b5c21c02852a // - args << Config::getInstance().getCodecOptions(); - args << Config::getInstance().getCodecName(); + args << QString("codec_options=%1").arg(Config::getInstance().getCodecOptions()); + args << QString("encoder_name=%1").arg(Config::getInstance().getCodecName()); #ifdef SERVER_DEBUGGER qInfo("Server debugger waiting for a client on device port " SERVER_DEBUGGER_PORT "..."); @@ -176,7 +176,7 @@ bool Server::execute() #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" + // mark: crop input format: "width:height:x:y" or "" for no crop, for example: "100:200:0:0" // 这条adb命令是阻塞运行的,m_serverProcess进程不会退出了 m_serverProcess.execute(m_params.serial, args); return true; diff --git a/QtScrcpy/device/server/server.h b/QtScrcpy/device/server/server.h index 496398b..1793576 100644 --- a/QtScrcpy/device/server/server.h +++ b/QtScrcpy/device/server/server.h @@ -31,7 +31,7 @@ public: quint16 maxSize = 720; // 视频分辨率 quint32 bitRate = 8000000; // 视频比特率 quint32 maxFps = 60; // 视频最大帧率 - QString crop = "-"; // 视频裁剪 + QString crop = ""; // 视频裁剪 bool control = true; // 安卓端是否接收键鼠控制 bool useReverse = true; // true:先使用adb reverse,失败后自动使用adb forward;false:直接使用adb forward int lockVideoOrientation = -1; // 是否锁定视频方向 diff --git a/QtScrcpy/util/config.cpp b/QtScrcpy/util/config.cpp index 60f6191..6c98b07 100644 --- a/QtScrcpy/util/config.cpp +++ b/QtScrcpy/util/config.cpp @@ -15,7 +15,7 @@ #define COMMON_PUSHFILE_DEF "/sdcard/" #define COMMON_SERVER_VERSION_KEY "ServerVersion" -#define COMMON_SERVER_VERSION_DEF "1.17" +#define COMMON_SERVER_VERSION_DEF "1.21" #define COMMON_SERVER_PATH_KEY "ServerPath" #define COMMON_SERVER_PATH_DEF "/data/local/tmp/scrcpy-server.jar" diff --git a/config/config.ini b/config/config.ini index 6b85845..7f61934 100644 --- a/config/config.ini +++ b/config/config.ini @@ -10,7 +10,7 @@ RenderExpiredFrames=0 # 视频解码方式:-1 自动,0 软解,1 dx硬解,2 opengl硬解 UseDesktopOpenGL=-1 # scrcpy-server的版本号(不要修改) -ServerVersion=1.17 +ServerVersion=1.21 # scrcpy-server推送到安卓设备的路径 ServerPath=/data/local/tmp/scrcpy-server.jar # 自定义adb路径,例如D:/android/tools/adb.exe @@ -18,10 +18,10 @@ AdbPath= # 编码选项 "-"表示默认 # 例如 CodecOptions="profile=1,level=2" # 更多编码选项参考 https://d.android.com/reference/android/media/MediaFormat -CodecOptions="-" +CodecOptions="" # 指定编码器名称,必须是H.264编码器 # 例如 CodecName="OMX.qcom.video.encoder.avc" -CodecName="-" +CodecName="OMX.qcom.video.encoder.avc" # Set the log level (debug, info, warn, error) LogLevel=info diff --git a/third_party/scrcpy-server b/third_party/scrcpy-server index ab38830e678d059bed1aa22dfb5475f9d516d35a..c04e18194911b632b051450707c8151cdedf7920 100644 GIT binary patch literal 40067 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 D?+;=H From 5e1cc1b44f07c90975d073a4901da561fc4355bf Mon Sep 17 00:00:00 2001 From: Barry <870709864@qq.com> Date: Sun, 9 Jan 2022 15:43:52 +0800 Subject: [PATCH 4/6] feat: add cmake (#544) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: cmake win * fix: win bug * feat: mac * feat: mac * feat: linux * feat: remove old cmake * fix: build bug * feat: ci mac build * feat: language * feat: build win * fix: win buid error * fix: mac build error * fix: mac publish error * feat: test * feat: remove ubuntu 16 * feat: cmake build on ubuntu * fix: ubuntu build error * feat: test * fix: build error on ubuntu * fix: build error * fix: build error * fix * fix * fix: 1 * fix: 2 * fix: 3 * f * a * b * 1 Co-authored-by: 冉坤 --- .github/workflows/macos.yml | 2 +- .github/workflows/ubuntu.yml | 5 +- .github/workflows/windows.yml | 2 +- .gitignore | 3 +- CMakeLists.txt | 19 +- QtScrcpy/CMakeLists.txt | 679 +++++++++++------- QtScrcpy/QtScrcpy.pro | 16 +- QtScrcpy/adb/CMakeLists.txt | 11 - QtScrcpy/common/CMakeLists.txt | 6 - QtScrcpy/device/CMakeLists.txt | 40 -- QtScrcpy/device/android/CMakeLists.txt | 7 - QtScrcpy/device/controller/CMakeLists.txt | 26 - .../controller/inputconvert/CMakeLists.txt | 39 - .../inputconvert/keymap/CMakeLists.txt | 8 - .../device/controller/receiver/CMakeLists.txt | 14 - QtScrcpy/device/decoder/CMakeLists.txt | 21 - QtScrcpy/device/filehandler/CMakeLists.txt | 9 - QtScrcpy/device/recorder/CMakeLists.txt | 14 - QtScrcpy/device/render/CMakeLists.txt | 8 - QtScrcpy/device/server/CMakeLists.txt | 20 - QtScrcpy/device/server/server.cpp | 18 +- QtScrcpy/device/stream/CMakeLists.txt | 20 - QtScrcpy/device/ui/CMakeLists.txt | 23 - QtScrcpy/devicemanage/CMakeLists.txt | 13 - QtScrcpy/fontawesome/CMakeLists.txt | 8 - QtScrcpy/main.cpp | 18 +- .../res/{Info_Mac.plist => Info_Mac.plist.in} | 8 +- QtScrcpy/uibase/CMakeLists.txt | 10 - QtScrcpy/util/CMakeLists.txt | 21 - QtScrcpy/util/mousetap/CMakeLists.txt | 50 -- QtScrcpy/version | 2 +- ci/linux/build_for_ubuntu.sh | 43 +- ci/mac/build_for_mac.sh | 46 +- ci/mac/publish_for_mac.sh | 2 +- ci/win/build_for_win.bat | 81 +-- ci/win/publish_for_win.bat | 10 +- config/config.ini | 6 +- 37 files changed, 521 insertions(+), 807 deletions(-) delete mode 100755 QtScrcpy/adb/CMakeLists.txt delete mode 100755 QtScrcpy/common/CMakeLists.txt delete mode 100755 QtScrcpy/device/CMakeLists.txt delete mode 100755 QtScrcpy/device/android/CMakeLists.txt delete mode 100755 QtScrcpy/device/controller/CMakeLists.txt delete mode 100755 QtScrcpy/device/controller/inputconvert/CMakeLists.txt delete mode 100755 QtScrcpy/device/controller/inputconvert/keymap/CMakeLists.txt delete mode 100755 QtScrcpy/device/controller/receiver/CMakeLists.txt delete mode 100755 QtScrcpy/device/decoder/CMakeLists.txt delete mode 100755 QtScrcpy/device/filehandler/CMakeLists.txt delete mode 100755 QtScrcpy/device/recorder/CMakeLists.txt delete mode 100755 QtScrcpy/device/render/CMakeLists.txt delete mode 100755 QtScrcpy/device/server/CMakeLists.txt delete mode 100755 QtScrcpy/device/stream/CMakeLists.txt delete mode 100755 QtScrcpy/device/ui/CMakeLists.txt delete mode 100755 QtScrcpy/devicemanage/CMakeLists.txt delete mode 100755 QtScrcpy/fontawesome/CMakeLists.txt rename QtScrcpy/res/{Info_Mac.plist => Info_Mac.plist.in} (90%) delete mode 100755 QtScrcpy/uibase/CMakeLists.txt delete mode 100755 QtScrcpy/util/CMakeLists.txt delete mode 100755 QtScrcpy/util/mousetap/CMakeLists.txt diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index ba7a815..1fc8b8d 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -44,7 +44,7 @@ jobs: ENV_QT_PATH: ${{ env.qt-install-path }} run: | python ci/generate-version.py - ci/mac/build_for_mac.sh release + ci/mac/build_for_mac.sh RelWithDebInfo # 获取ref最后一个/后的内容 - name: Get the version shell: bash diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 89ec6f8..5eca0d1 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -6,6 +6,7 @@ on: - 'QtScrcpy/**' - '!QtScrcpy/res/**' - '.github/workflows/ubuntu.yml' + - 'ci/linux/**' pull_request: paths: - 'QtScrcpy/**' @@ -17,7 +18,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-16.04,ubuntu-18.04] + os: [ubuntu-18.04] qt-ver: [5.15.1] qt-arch-install: [gcc_64] gcc-arch: [x64] @@ -47,4 +48,4 @@ jobs: ENV_QT_PATH: ${{ env.qt-install-path }} run: | python ci/generate-version.py - ci/linux/build_for_ubuntu.sh release + ci/linux/build_for_ubuntu.sh RelWithDebInfo diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index bfddbb2..027ae56 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -76,7 +76,7 @@ jobs: ENV_QT_PATH: ${{ env.qt-install-path }} run: | call python ci\generate-version.py - call "ci\win\build_for_win.bat" release ${{ matrix.msvc-arch }} + call "ci\win\build_for_win.bat" RelWithDebInfo ${{ matrix.msvc-arch }} # 获取ref最后一个/后的内容 - name: Get the version shell: bash diff --git a/.gitignore b/.gitignore index 93a2a8c..5a6f350 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ /build/ build-* *.DS_Store -userdata.ini \ No newline at end of file +userdata.ini +Info_Mac.plist \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 4666405..7762e2a 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,21 +1,4 @@ cmake_minimum_required(VERSION 3.19 FATAL_ERROR) - -# Read version numbers from file -file (STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/QtScrcpy/version STRING_VERSION) -message(STATUS "QtScrcpy Version ${STRING_VERSION}") -project(QtScrcpy - VERSION ${STRING_VERSION} - LANGUAGES C CXX -) - -if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - enable_language(OBJCXX) -endif() - -# Split version numbers -string(REPLACE "." ";" VERSION_LIST ${STRING_VERSION}) -list(GET VERSION_LIST 0 VERSION_MAJOR) -list(GET VERSION_LIST 1 VERSION_MINOR) -list(GET VERSION_LIST 2 VERSION_PATCH) +project(all) add_subdirectory(QtScrcpy) diff --git a/QtScrcpy/CMakeLists.txt b/QtScrcpy/CMakeLists.txt index 049cf62..fac6269 100755 --- a/QtScrcpy/CMakeLists.txt +++ b/QtScrcpy/CMakeLists.txt @@ -1,309 +1,434 @@ -set(CMAKE_AUTOUIC ON) -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) +# For VS2019 and Xcode 12+ support. +cmake_minimum_required(VERSION 3.19 FATAL_ERROR) +# +# Global config +# + +# QC is "Qt CMake" +# https://www.kdab.com/wp-content/uploads/stories/QTVTC20-Using-Modern-CMake-Kevin-Funk.pdf + +# QC Custom config +set(QC_PROJECT_NAME "QtScrcpy") +# Read version numbers from file +file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/version QC_FILE_VERSION) +set(QC_PROJECT_VERSION ${QC_FILE_VERSION}) + +# Project declare +project(${QC_PROJECT_NAME} VERSION ${QC_PROJECT_VERSION} LANGUAGES CXX) +message(STATUS "[${PROJECT_NAME}] Project ${PROJECT_NAME} ${PROJECT_VERSION}") + +# QC define + +# check arch +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(QC_CPU_ARCH x64) +else() + set(QC_CPU_ARCH x86) +endif() +message(STATUS "[${PROJECT_NAME}] CPU_ARCH:${QC_CPU_ARCH}") + +# CMake set +#set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets Network LinguistTools REQUIRED) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets Network LinguistTools REQUIRED) +# default RelWithDebInfo +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE RelWithDebInfo) +endif() +message(STATUS "[${PROJECT_NAME}] BUILD_TYPE:${CMAKE_BUILD_TYPE}") -if(MSVC) +# Compiler set +message(STATUS "[${PROJECT_NAME}] C++ compiler ID is: ${CMAKE_CXX_COMPILER_ID}") +if (MSVC) # FFmpeg cannot be compiled natively by MSVC version < 12.0 (2013) if(MSVC_VERSION LESS 1800) - message(FATAL_ERROR "[QtScrcpy] FATAL ERROR: MSVC version is older than 12.0 (2013).") + message(FATAL_ERROR "[${PROJECT_NAME}] ERROR: MSVC version is older than 12.0 (2013).") endif() - SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /utf-8") - SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /utf-8") + message(STATUS "[${PROJECT_NAME}] Set Warnings as error") + # warning level 3 and all warnings as errors + add_compile_options(/W3 /WX /wd4566) + + # avoid warning C4819 + add_compile_options(-source-charset:utf-8) + #add_compile_options(/utf-8) + + # ensure we use minimal "windows.h" lib without the crazy min max macros + add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN) + + # disable SAFESEH - avoid "LNK2026: module unsafe"(Qt5.15&&vs2019) + add_link_options(/SAFESEH:NO) endif() +if (NOT MSVC) + message(STATUS "[${PROJECT_NAME}] Set warnings as error") + # lots of warnings and all warnings as errors + add_compile_options(-Wall -Wextra -pedantic -Werror) -# ==================== macOS ==================== -if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - # QS_MAC_RESOURCES: esource file list stored in Contents/MacOS - file(GLOB QS_MAC_RESOURCES "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/lib/*.dylib") - list(APPEND QS_MAC_RESOURCES - "${PROJECT_SOURCE_DIR}/third_party/scrcpy-server" - "${PROJECT_SOURCE_DIR}/adb/mac/adb" - ) - - # QS_MAC_CONFIG: Config file stored in Contents/MacOS/config - set(QS_MAC_CONFIG "${PROJECT_SOURCE_DIR}/config/config.ini") + # disable some warning + add_compile_options(-Wno-nested-anon-types -Wno-c++17-extensions) endif() -set(QS_TS_FILES - ${CMAKE_CURRENT_SOURCE_DIR}/res/i18n/zh_CN.ts - ${CMAKE_CURRENT_SOURCE_DIR}/res/i18n/en_US.ts - ) -set_source_files_properties(${QS_TS_FILES} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/res/i18n") +# +# Qt +# -set(QS_SOURCES_MAIN +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets Network LinguistTools REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets Network LinguistTools REQUIRED) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + find_package(QT NAMES Qt6 Qt5 COMPONENTS X11Extras REQUIRED) + find_package(Qt${QT_VERSION_MAJOR} COMPONENTS X11Extras REQUIRED) +endif() + +message(STATUS "[${PROJECT_NAME}] Qt version is: ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}") + +# +# Sources +# + +# adb +set(QC_ADB_SOURCES + adb/adbprocess.h + adb/adbprocess.cpp +) +source_group(adb FILES ${QC_ADB_SOURCES}) + +# common +set(QC_COMMON_SOURCES + common/qscrcpyevent.h +) +source_group(common FILES ${QC_COMMON_SOURCES}) + +# device +set(QC_DEVICE_SOURCES + device/device.h + device/device.cpp + device/android/input.h + device/android/keycodes.h + device/controller/controller.h + device/controller/controller.cpp + device/controller/inputconvert/inputconvertbase.h + device/controller/inputconvert/inputconvertbase.cpp + device/controller/inputconvert/inputconvertnormal.h + device/controller/inputconvert/inputconvertnormal.cpp + device/controller/inputconvert/inputconvertgame.h + device/controller/inputconvert/inputconvertgame.cpp + device/controller/inputconvert/controlmsg.h + device/controller/inputconvert/controlmsg.cpp + device/controller/inputconvert/keymap/keymap.h + device/controller/inputconvert/keymap/keymap.cpp + device/controller/receiver/devicemsg.h + device/controller/receiver/devicemsg.cpp + device/controller/receiver/receiver.h + device/controller/receiver/receiver.cpp + device/decoder/avframeconvert.h + device/decoder/avframeconvert.cpp + device/decoder/decoder.h + device/decoder/decoder.cpp + device/decoder/fpscounter.h + device/decoder/fpscounter.cpp + device/decoder/videobuffer.h + device/decoder/videobuffer.cpp + device/filehandler/filehandler.h + device/filehandler/filehandler.cpp + device/recorder/recorder.h + device/recorder/recorder.cpp + device/render/qyuvopenglwidget.h + device/render/qyuvopenglwidget.cpp + device/server/server.h + device/server/server.cpp + device/server/tcpserver.h + device/server/tcpserver.cpp + device/server/videosocket.h + device/server/videosocket.cpp + device/stream/stream.h + device/stream/stream.cpp + device/ui/toolform.h + device/ui/toolform.cpp + device/ui/toolform.ui + device/ui/videoform.h + device/ui/videoform.cpp + device/ui/videoform.ui +) +source_group(device FILES ${QC_DEVICE_SOURCES}) + +# devicemanage +set(QC_DEVICEMANAGE_SOURCES + devicemanage/devicemanage.h + devicemanage/devicemanage.cpp +) +source_group(devicemanage FILES ${QC_DEVICEMANAGE_SOURCES}) + +# fontawesome +set(QC_FONTAWESOME_SOURCES + fontawesome/iconhelper.h + fontawesome/iconhelper.cpp +) +source_group(fontawesome FILES ${QC_FONTAWESOME_SOURCES}) + +# uibase +set(QC_UIBASE_SOURCES + uibase/keepratiowidget.h + uibase/keepratiowidget.cpp + uibase/magneticwidget.h + uibase/magneticwidget.cpp +) +source_group(uibase FILES ${QC_UIBASE_SOURCES}) + +# util +set(QC_UTIL_SOURCES + util/compat.h + util/config.h + util/config.cpp + util/bufferutil.h + util/bufferutil.cpp + util/mousetap/mousetap.h + util/mousetap/mousetap.cpp +) +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(QC_UTIL_SOURCES ${QC_UTIL_SOURCES} + util/mousetap/winmousetap.h + util/mousetap/winmousetap.cpp + ) +endif() +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(QC_UTIL_SOURCES ${QC_UTIL_SOURCES} + util/mousetap/xmousetap.h + util/mousetap/xmousetap.cpp + ) +endif() +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(QC_UTIL_SOURCES ${QC_UTIL_SOURCES} + util/mousetap/cocoamousetap.h + util/mousetap/cocoamousetap.mm + ) +endif() +source_group(util FILES ${QC_UTIL_SOURCES}) + +# qrc +set(QC_QRC_SOURCES "res/res.qrc") + +# main +set(QC_MAIN_SOURCES + main.cpp dialog.cpp dialog.h dialog.ui - ${QS_TS_FILES} + ${QC_QRC_SOURCES} ) -set(QS_QRC_MAIN "${CMAKE_CURRENT_SOURCE_DIR}/res/res.qrc") - -if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) # Qt version 6 - qt_create_translation(QS_QM_FILES ${CMAKE_CURRENT_SOURCE_DIR} ${QS_TS_FILES}) - - if(WIN32) - qt_add_executable(${CMAKE_PROJECT_NAME} WIN32 MANUAL_FINALIZATION - main.cpp - ${QS_SOURCES_MAIN} - ${QS_QRC_MAIN} - ) - - elseif(UNIX) - if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - qt_add_executable(${CMAKE_PROJECT_NAME} MACOSX_BUNDLE MANUAL_FINALIZATION - main.cpp - ${QS_SOURCES_MAIN} - ${QS_MAC_RESOURCES} - ${QS_MAC_CONFIG} - ${QS_QRC_MAIN} - ) - - else() - qt_add_executable(${CMAKE_PROJECT_NAME} MANUAL_FINALIZATION - main.cpp - ${QS_SOURCES_MAIN} - ${QS_QRC_MAIN} - ) - - endif() - endif() - -else() # Qt version 5 - qt5_create_translation(QS_QM_FILES ${CMAKE_CURRENT_SOURCE_DIR} ${QS_TS_FILES}) - - if(WIN32) - add_executable(${CMAKE_PROJECT_NAME} WIN32 - main.cpp - ${QS_SOURCES_MAIN} - ${QS_QRC_MAIN} - ) - elseif(UNIX) - if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - add_executable(${CMAKE_PROJECT_NAME} MACOSX_BUNDLE - main.cpp - ${QS_SOURCES_MAIN} - ${QS_MAC_RESOURCES} - ${QS_MAC_CONFIG} - ${QS_QRC_MAIN} - ) - else() - add_executable(${CMAKE_PROJECT_NAME} - main.cpp - ${QS_SOURCES_MAIN} - ${QS_QRC_MAIN} - ) - endif() - endif() - -endif() - -# ******************** Microsoft Windows ******************** -if(WIN32) - message(STATUS "[QtScrcpy] Make for Microsoft Windows.") - - # 通过rc的方式的话,VERSION变量rc中获取不到,定义为宏方便rc中使用 - # Define macros for .rc file +# plantform file +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + # Define VERSION macros for .rc file add_compile_definitions( - VERSION_MAJOR=${VERSION_MAJOR} - VERSION_MINOR=${VERSION_MINOR} - VERSION_PATCH=${VERSION_PATCH} - VERSION_RC_STR=\\\"${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}\\\" + VERSION_MAJOR=${PROJECT_VERSION_MAJOR} + VERSION_MINOR=${PROJECT_VERSION_MINOR} + VERSION_PATCH=${PROJECT_VERSION_PATCH} + VERSION_RC_STR="${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}" + ) + set(QC_PLANTFORM_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/res/${PROJECT_NAME}.rc" + ) +endif() +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + # Step 1. add icns to source file, for MACOSX_PACKAGE_LOCATION copy + set(QC_PLANTFORM_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/res/${PROJECT_NAME}.icns" ) - - if(CMAKE_SIZEOF_VOID_P EQUAL 8) # Compiler is 64-bit - message(STATUS "[QtScrcpy] 64-bit compiler detected.") - - set(QS_LIB_PATH "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/lib/x64") - if(CMAKE_BUILD_TYPE STREQUAL "Debug") - message(STATUS "[QtScrcpy] In debug mode.") - set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/win/x64/debug" - ) - else() - message(STATUS "[QtScrcpy] In release mode.") - set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/win/x64/release") - endif() - - set(QS_DLL_PATH "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/bin/x64") - - elseif(CMAKE_SIZEOF_VOID_P EQUAL 4) # Compiler is 32-bit - message(STATUS "[QtScrcpy] 32-bit compiler detected.") - - set(QS_LIB_PATH "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/lib/x86") - if(CMAKE_BUILD_TYPE STREQUAL "Debug") - message(STATUS "[QtScrcpy] In debug mode.") - set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/win/x86/debug") - else() - message(STATUS "[QtScrcpy] In release mode.") - set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/win/x86/release") - endif() - - set(QS_DLL_PATH "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/bin/x86") - endif() - - # 构建完成后复制DLL依赖库 - # Copy DLL dependencies after building - get_target_property(QS_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_PROJECT_NAME} RUNTIME_OUTPUT_DIRECTORY) - file(GLOB QS_DLL_FILES "${QS_DLL_PATH}/*.dll") - foreach(QS_DLL_FILE ${QS_DLL_FILES}) - add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD COMMAND - ${CMAKE_COMMAND} -E copy_if_different - "${QS_DLL_FILE}" "${QS_RUNTIME_OUTPUT_DIRECTORY}" - ) - endforeach() - - if(MSVC) - message(STATUS "[QtScrcpy] Microsoft Visual C++ is used.") - target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE ${QS_LIB_PATH}) - set(QS_EXTERNAL_LIBS_FFMPEG - avformat - avcodec - avutil - swscale - ) - # If MinGW is used, it is not appropriate to link static MSVC libs. - # Instead, we link DLLs directly - elseif(MINGW) - message(STATUS "[QtScrcpy] MinGW GCC is used.") - target_link_options(${CMAKE_PROJECT_NAME} PRIVATE - "-static" - ${QS_DLL_FILES} - "-Wl,--enable-stdcall-fixup" - ) - endif() - - set(RC_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/QtScrcpy.rc") - -# ******************** Unix-like OSs ******************** -elseif(UNIX) - set(QS_LIB_PATH "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/lib") - - # ==================== macOS ==================== - if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - message(STATUS "[QtScrcpy] Make for macOS.") - target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE ${QS_LIB_PATH}) - - if(CMAKE_BUILD_TYPE STREQUAL "Debug") - set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/mac/debug") - else() - set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/mac/release") - endif() - - # Icon file stored in Contents/Resources - set(QS_MAC_ICON_NAME "QtScrcpy.icns") - set(QS_MAC_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/res/${QS_MAC_ICON_NAME}") - - set_source_files_properties(${QS_MAC_RESOURCES} PROPERTIES - MACOSX_PACKAGE_LOCATION "MacOS" - ) - set_source_files_properties(${QS_MAC_CONFIG} PROPERTIES - MACOSX_PACKAGE_LOCATION "MacOS/config" - ) - - set(QS_EXTERNAL_LIBS_FFMPEG - avformat.58 - avcodec.58 - avutil.56 - swscale.5 - ) - - set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES - # The base plist template file - MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/res/Info_Mac.plist" - # The elements to be overwritten - MACOSX_BUNDLE_ICON_FILE "${QS_MAC_ICON_NAME}" - MACOSX_BUNDLE_BUNDLE_VERSION "${STRING_VERSION}" - MACOSX_BUNDLE_SHORT_VERSION_STRING "${STRING_VERSION}" - MACOSX_BUNDLE_LONG_VERSION_STRING "${STRING_VERSION}" - - # Copy file(s) to Contents/Resources - RESOURCE "${QS_MAC_ICON_PATH}" - ) - - # =============== Non-Mac OSs (Linux, BSD, etc.) =============== - else() - if(CMAKE_BUILD_TYPE STREQUAL "Debug") - set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/linux/debug") - else() - set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/linux/release") - endif() - - find_package(Threads REQUIRED) - - message(STATUS "[QtScrcpy] Make for non-Mac Unix-like OS.") - set(INSTALLED_FFMPEG_FOUND false) - - find_package(PkgConfig) - if(PkgConfig_FOUND) - pkg_check_modules(FFmpeg libavformat>=58 libavcodec>=58 libavutil>=56 libswscale>=5) - if(FFmpeg_FOUND) - set(INSTALLED_FFMPEG_FOUND true) - message(STATUS "[QtScrcpy] Development files of FFmpeg were detected in your OS and will be used.") - target_link_options(${CMAKE_PROJECT_NAME} PRIVATE ${FFmpeg_LDFLAGS}) - target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE ${FFmpeg_CFLAGS}) - set(QS_EXTERNAL_LIBS_FFMPEG ${FFmpeg_LIBRARIES}) - endif() - endif() - - if(NOT INSTALLED_FFMPEG_FOUND) - message(STATUS "[QtScrcpy] Development files of FFmpeg were not detected in your OS. Files within third_party/ffmpeg/ will be used.") - target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE ${QS_LIB_PATH}) - set(QS_EXTERNAL_LIBS_FFMPEG - avformat - avcodec - avutil - swscale - Threads::Threads - ) - endif() - endif() endif() -target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - -set(QS_SUBDIRECTORIES_MAIN - adb - common - device - devicemanage - fontawesome - uibase - util +# 使用qt5_add_translation 根据已有ts文件生成qm文件,不用qt5_create_translation +# 感兴趣可以了解下qt5_create_translation用法 https://www.cnblogs.com/apocelipes/p/14355460.html +set(QC_TS_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/res/i18n/zh_CN.ts + ${CMAKE_CURRENT_SOURCE_DIR}/res/i18n/en_US.ts ) -foreach(QS_SUBDIRECTORY_MAIN ${QS_SUBDIRECTORIES_MAIN}) - add_subdirectory(${QS_SUBDIRECTORY_MAIN}) -endforeach() +set_source_files_properties(${QC_TS_FILES} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/res/i18n") +qt5_add_translation(QC_QM_FILES ${QC_TS_FILES}) -target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC - adb - devicemanage - ) +# all sources +set(QC_PROJECT_SOURCES + ${QC_ADB_SOURCES} + ${QC_COMMON_SOURCES} + ${QC_DEVICE_SOURCES} + ${QC_DEVICEMANAGE_SOURCES} + ${QC_FONTAWESOME_SOURCES} + ${QC_UIBASE_SOURCES} + ${QC_UTIL_SOURCES} + ${QC_MAIN_SOURCES} + ${QC_PLANTFORM_SOURCES} +) -target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(QC_RUNTIME_TYPE MACOSX_BUNDLE) +endif() +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(QC_RUNTIME_TYPE WIN32) +endif() + + +add_executable(${PROJECT_NAME} ${QC_RUNTIME_TYPE} ${QC_PROJECT_SOURCES}) + +# +# Internal include path (todo: remove this, use absolute path include) +# + +target_include_directories(${PROJECT_NAME} PRIVATE adb) +target_include_directories(${PROJECT_NAME} PRIVATE common) +target_include_directories(${PROJECT_NAME} PRIVATE device) +target_include_directories(${PROJECT_NAME} PRIVATE device/filehandler) +target_include_directories(${PROJECT_NAME} PRIVATE device/android) +target_include_directories(${PROJECT_NAME} PRIVATE device/decoder) +target_include_directories(${PROJECT_NAME} PRIVATE device/controller) +target_include_directories(${PROJECT_NAME} PRIVATE device/controller/receiver) +target_include_directories(${PROJECT_NAME} PRIVATE device/controller/inputconvert) +target_include_directories(${PROJECT_NAME} PRIVATE device/controller/inputconvert/keymap) +target_include_directories(${PROJECT_NAME} PRIVATE device/server) +target_include_directories(${PROJECT_NAME} PRIVATE device/stream) +target_include_directories(${PROJECT_NAME} PRIVATE device/render) +target_include_directories(${PROJECT_NAME} PRIVATE device/ui) +target_include_directories(${PROJECT_NAME} PRIVATE device/recorder) +target_include_directories(${PROJECT_NAME} PRIVATE devicemanage) +target_include_directories(${PROJECT_NAME} PRIVATE fontawesome) +target_include_directories(${PROJECT_NAME} PRIVATE util) +target_include_directories(${PROJECT_NAME} PRIVATE uibase) + +# +# common deps +# + +# Qt +target_link_libraries(${PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network - device - stream - ui - util +) + +# output dir +# https://cmake.org/cmake/help/latest/prop_gbl/GENERATOR_IS_MULTI_CONFIG.html +get_property(QC_IS_MUTIL_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +message(STATUS "multi config:" QC_IS_MUTIL_CONFIG) + +# $<0:> 使用生成器表达式为每个config设置RUNTIME_OUTPUT_DIRECTORY,这样multi config就不会自动追加CMAKE_BUILD_TYPE子目录了 +# 1. multi config介绍 https://cmake.org/cmake/help/latest/prop_gbl/GENERATOR_IS_MULTI_CONFIG.html +# 2. multi config在不用表达式生成器时自动追加子目录说明 https://cmake.org/cmake/help/latest/prop_tgt/RUNTIME_OUTPUT_DIRECTORY.html +# 3. 使用表达式生成器禁止multi config自动追加子目录解决方案 https://stackoverflow.com/questions/7747857/in-cmake-how-do-i-work-around-the-debug-and-release-directories-visual-studio-2 +set_target_properties(${PROJECT_NAME} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../output/${QC_CPU_ARCH}/${CMAKE_BUILD_TYPE}/$<0:>" +) + +# +# plantform deps +# + +# windows +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + # ffmpeg + # include + target_include_directories(${PROJECT_NAME} PRIVATE ../third_party/ffmpeg/include) + # link + set(FFMPEG_LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib/${QC_CPU_ARCH}") + target_link_directories(${PROJECT_NAME} PRIVATE ${FFMPEG_LIB_PATH}) + target_link_libraries(${PROJECT_NAME} PRIVATE + avformat + avcodec + avutil + swscale + ) + # copy + set(FFMPEG_BIN_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/bin/${QC_CPU_ARCH}") + get_target_property(FFMPEG_BIN_OUTPUT_PATH ${PROJECT_NAME} RUNTIME_OUTPUT_DIRECTORY) + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FFMPEG_BIN_PATH}/avcodec-58.dll" "${FFMPEG_BIN_OUTPUT_PATH}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FFMPEG_BIN_PATH}/avformat-58.dll" "${FFMPEG_BIN_OUTPUT_PATH}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FFMPEG_BIN_PATH}/avutil-56.dll" "${FFMPEG_BIN_OUTPUT_PATH}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FFMPEG_BIN_PATH}/swscale-5.dll" "${FFMPEG_BIN_OUTPUT_PATH}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FFMPEG_BIN_PATH}/swresample-3.dll" "${FFMPEG_BIN_OUTPUT_PATH}" + ) +endif() + +# MacOS +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + # ffmpeg + # include + target_include_directories(${PROJECT_NAME} PRIVATE ../third_party/ffmpeg/include) + # link + set(FFMPEG_LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib") + target_link_directories(${PROJECT_NAME} PRIVATE ${FFMPEG_LIB_PATH}) + target_link_libraries(${PROJECT_NAME} PRIVATE + avformat.58 + avcodec.58 + avutil.56 + swscale.5 ) -if(QT_VERSION_MAJOR EQUAL 6) - qt_finalize_executable(${CMAKE_PROJECT_NAME}) + # copy bundle file + get_target_property(MACOS_BUNDLE_PATH ${PROJECT_NAME} RUNTIME_OUTPUT_DIRECTORY) + set(MACOS_BUNDLE_PATH ${MACOS_BUNDLE_PATH}/${PROJECT_NAME}.app/Contents) + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + # dylib,scrcpy-server,adb copy to Contents/MacOS + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib/libavcodec.58.dylib" "${MACOS_BUNDLE_PATH}/MacOS" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib/libavformat.58.dylib" "${MACOS_BUNDLE_PATH}/MacOS" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib/libavutil.56.dylib" "${MACOS_BUNDLE_PATH}/MacOS" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib/libswscale.5.dylib" "${MACOS_BUNDLE_PATH}/MacOS" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib/libswresample.3.dylib" "${MACOS_BUNDLE_PATH}/MacOS" + + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/scrcpy-server" "${MACOS_BUNDLE_PATH}/MacOS" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/adb/mac/adb" "${MACOS_BUNDLE_PATH}/MacOS" + # config file copy to Contents/MacOS/config + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../config/config.ini" "${MACOS_BUNDLE_PATH}/MacOS/config" + ) + + # Step 2. ues MACOSX_PACKAGE_LOCATION copy icns to Resources + set_source_files_properties( + ${CMAKE_CURRENT_SOURCE_DIR}/res/${PROJECT_NAME}.icns + PROPERTIES MACOSX_PACKAGE_LOCATION Resources + ) + + # use MACOSX_BUNDLE_INFO_PLIST custom plist, not use MACOSX_BUNDLE_BUNDLE_NAME etc.. + set(INFO_PLIST_TEMPLATE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/Info_Mac.plist.in") + set(INFO_PLIST_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/Info_Mac.plist") + file(READ "${INFO_PLIST_TEMPLATE_FILE}" plist_contents) + string(REPLACE "\${BUNDLE_VERSION}" "${PROJECT_VERSION}" plist_contents ${plist_contents}) + file(WRITE ${INFO_PLIST_FILE} ${plist_contents}) + set_target_properties(${PROJECT_NAME} PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${INFO_PLIST_FILE}" + # "" disable code sign + XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "" + ) + + # mac framework + target_link_libraries(${PROJECT_NAME} PRIVATE "-framework AppKit") endif() + +# Linux +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) + + # include + target_include_directories(${PROJECT_NAME} PRIVATE ../third_party/ffmpeg/include) + # link + set(FFMPEG_LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/ffmpeg/lib") + target_link_directories(${PROJECT_NAME} PRIVATE ${FFMPEG_LIB_PATH}) + target_link_libraries(${PROJECT_NAME} PRIVATE + # ffmpeg + avformat + avcodec + avutil + swscale + # qx11 + Qt${QT_VERSION_MAJOR}::X11Extras + # xcb https://doc.qt.io/qt-5/linux-requirements.html + xcb + # pthread + Threads::Threads + ) + + # linux set app icon: https://blog.csdn.net/MrNoboday/article/details/82870853 +endif() \ No newline at end of file diff --git a/QtScrcpy/QtScrcpy.pro b/QtScrcpy/QtScrcpy.pro index 990537e..720f018 100644 --- a/QtScrcpy/QtScrcpy.pro +++ b/QtScrcpy/QtScrcpy.pro @@ -97,9 +97,9 @@ win32 { message("x64") # 输出目录 CONFIG(debug, debug|release) { - DESTDIR = $$PWD/../output/win/x64/debug + DESTDIR = $$PWD/../output/x64/debug } else { - DESTDIR = $$PWD/../output/win/x64/release + DESTDIR = $$PWD/../output/x64/release } # 依赖模块 @@ -114,9 +114,9 @@ win32 { message("x86") # 输出目录 CONFIG(debug, debug|release) { - DESTDIR = $$PWD/../output/win/x86/debug + DESTDIR = $$PWD/../output/x86/debug } else { - DESTDIR = $$PWD/../output/win/x86/release + DESTDIR = $$PWD/../output/x86/release } # 依赖模块 @@ -147,9 +147,9 @@ win32 { macos { # 输出目录 CONFIG(debug, debug|release) { - DESTDIR = $$PWD/../output/mac/debug + DESTDIR = $$PWD/../output/debug } else { - DESTDIR = $$PWD/../output/mac/release + DESTDIR = $$PWD/../output/release } # 依赖模块 @@ -196,9 +196,9 @@ macos { linux { # 输出目录 CONFIG(debug, debug|release) { - DESTDIR = $$PWD/../output/linux/debug + DESTDIR = $$PWD/../output/debug } else { - DESTDIR = $$PWD/../output/linux/release + DESTDIR = $$PWD/../output/release } # 依赖模块 diff --git a/QtScrcpy/adb/CMakeLists.txt b/QtScrcpy/adb/CMakeLists.txt deleted file mode 100755 index 3644ade..0000000 --- a/QtScrcpy/adb/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -set(QS_SOURCES_ADB - adbprocess.h - adbprocess.cpp -) - -add_library(adb ${QS_SOURCES_ADB}) -target_include_directories(adb PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(adb PRIVATE - Qt${QT_VERSION_MAJOR}::Widgets - util - ) diff --git a/QtScrcpy/common/CMakeLists.txt b/QtScrcpy/common/CMakeLists.txt deleted file mode 100755 index cd2b0c6..0000000 --- a/QtScrcpy/common/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -set(QS_SOURCES_COMMON - qscrcpyevent.h -) - -add_library(common INTERFACE ${QS_SOURCES_COMMON}) -target_include_directories(common INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/QtScrcpy/device/CMakeLists.txt b/QtScrcpy/device/CMakeLists.txt deleted file mode 100755 index 6e8b837..0000000 --- a/QtScrcpy/device/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -set(QS_SUBDIRECTORIES_DEVICE - android - controller - decoder - filehandler - recorder - render - server - stream - ui -) - -set(QS_SOURCES_DEVICE - device.h - device.cpp -) - -add_library(device ${QS_SOURCES_DEVICE}) - -target_include_directories(device PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - -foreach(QS_SUBDIRECTORY_DEVICE ${QS_SUBDIRECTORIES_DEVICE}) - add_subdirectory (${QS_SUBDIRECTORY_DEVICE}) -endforeach() - -target_link_libraries(device INTERFACE inputconvert) -target_link_libraries(device PRIVATE - Qt${QT_VERSION_MAJOR}::Widgets - # device (self) - controller - decoder - filehandler - recorder - server - stream - ui - util - mousetap - ) - diff --git a/QtScrcpy/device/android/CMakeLists.txt b/QtScrcpy/device/android/CMakeLists.txt deleted file mode 100755 index 432d749..0000000 --- a/QtScrcpy/device/android/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -set(QS_SOURCES_DEVICE_ANDROID - input.h - keycodes.h -) - -add_library(android INTERFACE ${QS_SOURCES_DEVICE_ANDROID}) -target_include_directories(android INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/QtScrcpy/device/controller/CMakeLists.txt b/QtScrcpy/device/controller/CMakeLists.txt deleted file mode 100755 index cf34254..0000000 --- a/QtScrcpy/device/controller/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -SET(QS_SUBDIRECTORIES_DEVICE_CONTROLLER - inputconvert - receiver -) - -SET(QS_SOURCES_DEVICE_CONTROLLER - controller.h - controller.cpp -) - -add_library(controller ${QS_SOURCES_DEVICE_CONTROLLER}) - -target_include_directories(controller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - -foreach(QS_SUBDIRECTORY_DEVICE_CONTROLLER ${QS_SUBDIRECTORIES_DEVICE_CONTROLLER}) - add_subdirectory (${QS_SUBDIRECTORY_DEVICE_CONTROLLER}) -endforeach() - -target_link_libraries(controller PUBLIC - inputconvert - ) -target_link_libraries(controller PRIVATE - Qt${QT_VERSION_MAJOR}::Widgets - receiver - server - ) diff --git a/QtScrcpy/device/controller/inputconvert/CMakeLists.txt b/QtScrcpy/device/controller/inputconvert/CMakeLists.txt deleted file mode 100755 index 2c5cd44..0000000 --- a/QtScrcpy/device/controller/inputconvert/CMakeLists.txt +++ /dev/null @@ -1,39 +0,0 @@ -SET(QS_SUBDIRECTORIES_DEVICE_CONTROLLER_INPUTCONVERT - keymap -) - -set(QS_SOURCES_DEVICE_CONTROLLER_INPUTCONVERT - controlmsg.h - controlmsg.cpp - inputconvertbase.h - inputconvertbase.cpp - inputconvertgame.h - inputconvertgame.cpp - inputconvertnormal.h - inputconvertnormal.cpp -) - -add_library(inputconvert ${QS_SOURCES_DEVICE_CONTROLLER_INPUTCONVERT}) - -target_include_directories(inputconvert PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - -foreach(QS_SUBDIRECTORY_DEVICE_CONTROLLER_INPUTCONVERT ${QS_SUBDIRECTORIES_DEVICE_CONTROLLER_INPUTCONVERT}) - add_subdirectory (${QS_SUBDIRECTORY_DEVICE_CONTROLLER_INPUTCONVERT}) -endforeach() - -target_link_libraries(inputconvert PUBLIC - common - # controller - android - ) -target_link_libraries(inputconvert INTERFACE - # controller - # inputconvert (self) - keymap - ) -target_link_libraries(inputconvert PRIVATE - Qt${QT_VERSION_MAJOR}::Widgets - - controller - util - ) diff --git a/QtScrcpy/device/controller/inputconvert/keymap/CMakeLists.txt b/QtScrcpy/device/controller/inputconvert/keymap/CMakeLists.txt deleted file mode 100755 index a74b052..0000000 --- a/QtScrcpy/device/controller/inputconvert/keymap/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -set(QS_SOURCES_DEVICE_CONTROLLER_INPUTCONVERT_KEYMAP - keymap.h - keymap.cpp -) - -add_library(keymap ${QS_SOURCES_DEVICE_CONTROLLER_INPUTCONVERT_KEYMAP}) -target_include_directories(keymap PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(keymap PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) diff --git a/QtScrcpy/device/controller/receiver/CMakeLists.txt b/QtScrcpy/device/controller/receiver/CMakeLists.txt deleted file mode 100755 index ee6ced2..0000000 --- a/QtScrcpy/device/controller/receiver/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -SET(QS_SOURCES_DEVICE_CONTROLLER_RECEIVER - devicemsg.h - devicemsg.cpp - receiver.h - receiver.cpp - ) - -add_library(receiver ${QS_SOURCES_DEVICE_CONTROLLER_RECEIVER}) -target_include_directories(receiver PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(receiver PRIVATE - Qt${QT_VERSION_MAJOR}::Widgets - Qt${QT_VERSION_MAJOR}::Network - util - ) diff --git a/QtScrcpy/device/decoder/CMakeLists.txt b/QtScrcpy/device/decoder/CMakeLists.txt deleted file mode 100755 index b2931a2..0000000 --- a/QtScrcpy/device/decoder/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -set(QS_SOURCES_DEVICE_DECODER - avframeconvert.h - avframeconvert.cpp - decoder.h - decoder.cpp - fpscounter.h - fpscounter.cpp - videobuffer.h - videobuffer.cpp -) - -add_library(decoder ${QS_SOURCES_DEVICE_DECODER}) -target_include_directories(decoder PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} - "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/include" - ) -target_link_libraries(decoder PUBLIC ${QS_EXTERNAL_LIBS_FFMPEG}) -target_link_libraries(decoder PRIVATE - Qt${QT_VERSION_MAJOR}::Widgets - util - ) diff --git a/QtScrcpy/device/filehandler/CMakeLists.txt b/QtScrcpy/device/filehandler/CMakeLists.txt deleted file mode 100755 index 17de9e2..0000000 --- a/QtScrcpy/device/filehandler/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -set(QS_SOURCES_DEVICE_FILEHANDLER - filehandler.h - filehandler.cpp -) - -add_library(filehandler ${QS_SOURCES_DEVICE_FILEHANDLER}) -target_include_directories(filehandler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(filehandler PUBLIC adb) -target_link_libraries(filehandler PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) diff --git a/QtScrcpy/device/recorder/CMakeLists.txt b/QtScrcpy/device/recorder/CMakeLists.txt deleted file mode 100755 index 12144db..0000000 --- a/QtScrcpy/device/recorder/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -set(QS_SOURCES_DEVICE_RECORDER - recorder.h - recorder.cpp -) - -add_library(recorder ${QS_SOURCES_DEVICE_RECORDER}) -target_include_directories(recorder PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} - "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/include" - ) -target_link_libraries(recorder PRIVATE - Qt${QT_VERSION_MAJOR}::Widgets - util - ) diff --git a/QtScrcpy/device/render/CMakeLists.txt b/QtScrcpy/device/render/CMakeLists.txt deleted file mode 100755 index ee3a140..0000000 --- a/QtScrcpy/device/render/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -set(QS_SOURCES_DEVICE_RENDER - qyuvopenglwidget.h - qyuvopenglwidget.cpp -) - -add_library(render ${QS_SOURCES_DEVICE_RENDER}) -target_include_directories(render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(render PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) diff --git a/QtScrcpy/device/server/CMakeLists.txt b/QtScrcpy/device/server/CMakeLists.txt deleted file mode 100755 index eb7fe84..0000000 --- a/QtScrcpy/device/server/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -set(QS_SOURCES_DEVICE_SERVER - server.h - server.cpp - tcpserver.h - tcpserver.cpp - videosocket.h - videosocket.cpp -) - -add_library(server ${QS_SOURCES_DEVICE_SERVER}) -target_include_directories(server PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(server PUBLIC - Qt${QT_VERSION_MAJOR}::Network - adb - ) -target_link_libraries(server PRIVATE - Qt${QT_VERSION_MAJOR}::Widgets - common - util - ) diff --git a/QtScrcpy/device/server/server.cpp b/QtScrcpy/device/server/server.cpp index 756da4b..1343b33 100644 --- a/QtScrcpy/device/server/server.cpp +++ b/QtScrcpy/device/server/server.cpp @@ -142,18 +142,18 @@ bool Server::execute() args << "/"; // unused; args << "com.genymobile.scrcpy.Server"; args << Config::getInstance().getServerVersion(); - args << QString("log_level=%1").arg(Config::getInstance().getLogLevel()); + + if (!Config::getInstance().getLogLevel().isEmpty()) { + args << QString("log_level=%1").arg(Config::getInstance().getLogLevel()); + } args << QString("max_size=%1").arg(QString::number(m_params.maxSize)); args << QString("bit_rate=%1").arg(QString::number(m_params.bitRate)); args << QString("max_fps=%1").arg(QString::number(m_params.maxFps)); args << QString("lock_video_orientation=%1").arg(QString::number(m_params.lockVideoOrientation)); args << QString("tunnel_forward=%1").arg((m_tunnelForward ? "true" : "false")); - if (m_params.crop.isEmpty()) { - args << "crop="; - } else { + if (!m_params.crop.isEmpty()) { args << QString("crop=%1").arg(m_params.crop); } - args << "send_frame_meta=true"; // always send frame meta (packet boundaries + timestamp) args << QString("control=%1").arg((m_params.control ? "true" : "false")); args << "display_id=0"; // display id args << "show_touches=false"; // show touch @@ -161,8 +161,12 @@ bool Server::execute() // code option // https://github.com/Genymobile/scrcpy/commit/080a4ee3654a9b7e96c8ffe37474b5c21c02852a // - args << QString("codec_options=%1").arg(Config::getInstance().getCodecOptions()); - args << QString("encoder_name=%1").arg(Config::getInstance().getCodecName()); + if (Config::getInstance().getCodecOptions() != "") { + args << QString("codec_options=%1").arg(Config::getInstance().getCodecOptions()); + } + if (Config::getInstance().getCodecName() != "") { + args << QString("encoder_name=%1").arg(Config::getInstance().getCodecName()); + } #ifdef SERVER_DEBUGGER qInfo("Server debugger waiting for a client on device port " SERVER_DEBUGGER_PORT "..."); diff --git a/QtScrcpy/device/stream/CMakeLists.txt b/QtScrcpy/device/stream/CMakeLists.txt deleted file mode 100755 index 469d2d3..0000000 --- a/QtScrcpy/device/stream/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -set(QS_SOURCES_DEVICE_STREAM - stream.h - stream.cpp -) - -add_library(stream ${QS_SOURCES_DEVICE_STREAM}) -target_include_directories(stream PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} - "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/include" - ) - -target_link_libraries(stream PUBLIC ${QS_EXTERNAL_LIBS_FFMPEG}) -target_link_libraries(stream PRIVATE - Qt${QT_VERSION_MAJOR}::Widgets - #controller - decoder - recorder - server - util - ) diff --git a/QtScrcpy/device/ui/CMakeLists.txt b/QtScrcpy/device/ui/CMakeLists.txt deleted file mode 100755 index f8f6325..0000000 --- a/QtScrcpy/device/ui/CMakeLists.txt +++ /dev/null @@ -1,23 +0,0 @@ -set(QS_SOURCES_DEVICE_UI - toolform.h - toolform.cpp - toolform.ui - videoform.h - videoform.cpp - videoform.ui -) - -add_library(ui ${QS_SOURCES_DEVICE_UI}) -target_include_directories(ui PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_include_directories(ui PRIVATE "${PROJECT_SOURCE_DIR}/third_party/ffmpeg/include") -target_link_libraries(ui PUBLIC - device - uibase - ) -target_link_libraries(ui PRIVATE - Qt${QT_VERSION_MAJOR}::Widgets - controller - render - fontawesome - util - ) diff --git a/QtScrcpy/devicemanage/CMakeLists.txt b/QtScrcpy/devicemanage/CMakeLists.txt deleted file mode 100755 index 8ce0ab7..0000000 --- a/QtScrcpy/devicemanage/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -set(QS_SOURCES_DEVICEMANAGE - devicemanage.h - devicemanage.cpp -) - -add_library(devicemanage ${QS_SOURCES_DEVICEMANAGE}) -target_include_directories(devicemanage PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(devicemanage PUBLIC device) -target_link_libraries(devicemanage PRIVATE - Qt${QT_VERSION_MAJOR}::Widgets - server - ui - ) diff --git a/QtScrcpy/fontawesome/CMakeLists.txt b/QtScrcpy/fontawesome/CMakeLists.txt deleted file mode 100755 index ead0947..0000000 --- a/QtScrcpy/fontawesome/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -set(QS_SOURCES_FONTAWESOME - iconhelper.h - iconhelper.cpp -) - -add_library(fontawesome ${QS_SOURCES_FONTAWESOME}) -target_include_directories(fontawesome PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(fontawesome PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) diff --git a/QtScrcpy/main.cpp b/QtScrcpy/main.cpp index 9295805..31b501d 100644 --- a/QtScrcpy/main.cpp +++ b/QtScrcpy/main.cpp @@ -23,21 +23,21 @@ int main(int argc, char *argv[]) { // set env #ifdef Q_OS_WIN32 - qputenv("QTSCRCPY_ADB_PATH", "../../../../third_party/adb/win/adb.exe"); - qputenv("QTSCRCPY_SERVER_PATH", "../../../../third_party/scrcpy-server"); - qputenv("QTSCRCPY_KEYMAP_PATH", "../../../../keymap"); - qputenv("QTSCRCPY_CONFIG_PATH", "../../../../config"); + qputenv("QTSCRCPY_ADB_PATH", "../../../third_party/adb/win/adb.exe"); + qputenv("QTSCRCPY_SERVER_PATH", "../../../third_party/scrcpy-server"); + qputenv("QTSCRCPY_KEYMAP_PATH", "../../../keymap"); + qputenv("QTSCRCPY_CONFIG_PATH", "../../../config"); #endif #ifdef Q_OS_OSX - qputenv("QTSCRCPY_KEYMAP_PATH", "../../../../../../keymap"); + qputenv("QTSCRCPY_KEYMAP_PATH", "../../../../../keymap"); #endif #ifdef Q_OS_LINUX - qputenv("QTSCRCPY_ADB_PATH", "../../../third_party/adb/linux/adb"); - qputenv("QTSCRCPY_SERVER_PATH", "../../../third_party/scrcpy-server"); - qputenv("QTSCRCPY_CONFIG_PATH", "../../../config"); - qputenv("QTSCRCPY_KEYMAP_PATH", "../../../keymap"); + qputenv("QTSCRCPY_ADB_PATH", "../../third_party/adb/linux/adb"); + qputenv("QTSCRCPY_SERVER_PATH", "../../third_party/scrcpy-server"); + qputenv("QTSCRCPY_CONFIG_PATH", "../../config"); + qputenv("QTSCRCPY_KEYMAP_PATH", "../../keymap"); #endif g_msgType = covertLogLevel(Config::getInstance().getLogLevel()); diff --git a/QtScrcpy/res/Info_Mac.plist b/QtScrcpy/res/Info_Mac.plist.in similarity index 90% rename from QtScrcpy/res/Info_Mac.plist rename to QtScrcpy/res/Info_Mac.plist.in index 3e25f20..007a4b7 100644 --- a/QtScrcpy/res/Info_Mac.plist +++ b/QtScrcpy/res/Info_Mac.plist.in @@ -5,11 +5,11 @@ CFBundleDevelopmentRegion zh-Hans CFBundleExecutable - @EXECUTABLE@ + QtScrcpy CFBundleGetInfoString Created by rankun CFBundleIconFile - @ICON@ + QtScrcpy CFBundleIdentifier rankun.QtScrcpy CFBundleInfoDictionaryVersion @@ -19,13 +19,13 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.0 + ${BUNDLE_VERSION} CFBundleSupportedPlatforms MacOSX CFBundleVersion - 1.0.0 + ${BUNDLE_VERSION} LSMinimumSystemVersion 10.10 NSAppleEventsUsageDescription diff --git a/QtScrcpy/uibase/CMakeLists.txt b/QtScrcpy/uibase/CMakeLists.txt deleted file mode 100755 index 861883a..0000000 --- a/QtScrcpy/uibase/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -set(QS_SOURCES_UIBASE - keepratiowidget.h - keepratiowidget.cpp - magneticwidget.h - magneticwidget.cpp -) - -add_library(uibase ${QS_SOURCES_UIBASE}) -target_include_directories(uibase PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(uibase PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) diff --git a/QtScrcpy/util/CMakeLists.txt b/QtScrcpy/util/CMakeLists.txt deleted file mode 100755 index 2d92d13..0000000 --- a/QtScrcpy/util/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -set(QS_SUBDIRECTORIES_UTIL - mousetap -) - -set(QS_SOURCES_UTIL - bufferutil.h - bufferutil.cpp - compat.h - config.h - config.cpp -) - -add_library(util ${QS_SOURCES_UTIL}) - -target_include_directories(util PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - -foreach(QS_SUBDIRECTORY_UTIL ${QS_SUBDIRECTORIES_UTIL}) - add_subdirectory (${QS_SUBDIRECTORY_UTIL}) -endforeach() - -target_link_libraries(util PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) diff --git a/QtScrcpy/util/mousetap/CMakeLists.txt b/QtScrcpy/util/mousetap/CMakeLists.txt deleted file mode 100755 index 846851b..0000000 --- a/QtScrcpy/util/mousetap/CMakeLists.txt +++ /dev/null @@ -1,50 +0,0 @@ -set(QS_SOURCES_UTIL_MOUSETAP - mousetap.h - mousetap.cpp -) - -# Microsoft Windows -if(WIN32) - - list(APPEND QS_SOURCES_UTIL_MOUSETAP - winmousetap.h - winmousetap.cpp - ) - set(QS_EXTERNAL_LIBS_UTIL_MOUSETAP User32) - -elseif(UNIX) -# macOS - if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - - find_library(APPKIT AppKit REQUIRED) - set(QS_EXTERNAL_LIBS_UTIL_MOUSETAP ${APPKIT}) - - list(APPEND QS_SOURCES_UTIL_MOUSETAP - cocoamousetap.h - cocoamousetap.mm - ) - - target_compile_options(mousetap "-mmacosx-version-min=10.6") - - # Linux, BSD, etc. - else() - - find_package(QT NAMES Qt6 Qt5 COMPONENTS X11Extras REQUIRED) - find_package(Qt${QT_VERSION_MAJOR} COMPONENTS X11Extras REQUIRED) - set(QS_EXTERNAL_LIBS_UTIL_MOUSETAP - Qt${QT_VERSION_MAJOR}::X11Extras - xcb - ) - - list(APPEND QS_SOURCES_UTIL_MOUSETAP - xmousetap.h - xmousetap.cpp - ) - - endif() -endif() - -add_library(mousetap ${QS_SOURCES_UTIL_MOUSETAP}) -target_link_libraries(mousetap PRIVATE - Qt${QT_VERSION_MAJOR}::Widgets - ${QS_EXTERNAL_LIBS_UTIL_MOUSETAP}) diff --git a/QtScrcpy/version b/QtScrcpy/version index dc1e644..77d6f4c 100644 --- a/QtScrcpy/version +++ b/QtScrcpy/version @@ -1 +1 @@ -1.6.0 +0.0.0 diff --git a/ci/linux/build_for_ubuntu.sh b/ci/linux/build_for_ubuntu.sh index b67c72e..5e21bb1 100755 --- a/ci/linux/build_for_ubuntu.sh +++ b/ci/linux/build_for_ubuntu.sh @@ -8,7 +8,7 @@ echo --------------------------------------------------------------- # 从环境变量获取必要参数 # 例如 /home/barry/Qt5.9.6/5.9.6 echo ENV_QT_PATH $ENV_QT_PATH -qt_gcc_path=$ENV_QT_PATH/gcc_64 +qt_cmake_path=$ENV_QT_PATH/gcc_64/lib/cmake/Qt5 # 获取绝对路径,保证其他目录执行此脚本依然正确 { @@ -21,17 +21,17 @@ old_cd=$(pwd) cd $(dirname "$0") # 启动参数声明 -build_mode=debug +build_mode=RelWithDebInfo echo echo echo --------------------------------------------------------------- -echo check build param[debug/release] +echo check build param[Debug/Release/MinSizeRel/RelWithDebInfo] echo --------------------------------------------------------------- # 编译参数检查 -build_mode=$(echo $1 | tr '[:upper:]' '[:lower:]') -if [[ $build_mode != "release" && $build_mode != "debug" ]]; then +build_mode=$(echo $1) +if [[ $build_mode != "Release" && $build_mode != "Debug" && $build_mode != "MinSizeRel" && $build_mode != "RelWithDebInfo" ]]; then echo "error: unkonow build mode -- $1" exit 1 fi @@ -40,44 +40,37 @@ fi echo current build mode: $build_mode # 环境变量设置 -export PATH=$qt_gcc_path/bin:$PATH +#export PATH=$qt_gcc_path/bin:$PATH echo echo echo --------------------------------------------------------------- -echo begin qmake build +echo begin cmake build echo --------------------------------------------------------------- # 删除输出目录 -output_path=$script_path../../output/linux/$build_mode +output_path=$script_path../../output if [ -d "$output_path" ]; then rm -rf $output_path fi # 删除临时目录 -temp_path=$script_path/../temp -if [ -d "$temp_path" ]; then - rm -rf $temp_path +build_path=$script_path/../build_temp +if [ -d "$build_path" ]; then + rm -rf $build_path fi -mkdir $temp_path -cd $temp_path +mkdir $build_path +cd $build_path -qmake_params="-spec linux-g++" -if [ $build_mode == "debug" ]; then - qmake_params="$qmake_params CONFIG+=debug CONFIG+=x86_64 CONFIG+=qml_debug" -else - qmake_params="$qmake_params CONFIG+=x86_64 CONFIG+=qtquickcompiler" -fi - -# qmake ../../all.pro -spec linux-g++ CONFIG+=debug CONFIG+=x86_64 CONFIG+=qml_debug -qmake ../../all.pro $qmake_params +cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode" +cmake $cmake_params ../.. if [ $? -ne 0 ] ;then - echo "qmake failed" + echo "cmake failed" exit 1 fi -make -j8 +cmake --build . --config $build_mode -j8 if [ $? -ne 0 ] ;then - echo "make failed" + echo "cmake build failed" exit 1 fi diff --git a/ci/mac/build_for_mac.sh b/ci/mac/build_for_mac.sh index aa5990f..a3e6dd0 100755 --- a/ci/mac/build_for_mac.sh +++ b/ci/mac/build_for_mac.sh @@ -8,7 +8,7 @@ echo --------------------------------------------------------------- # 从环境变量获取必要参数 # 例如 /Users/barry/Qt5.12.5/5.12.5 echo ENV_QT_PATH $ENV_QT_PATH -qt_clang_path=$ENV_QT_PATH/clang_64 +qt_cmake_path=$ENV_QT_PATH/clang_64/lib/cmake/Qt5 # 获取绝对路径,保证其他目录执行此脚本依然正确 { @@ -21,17 +21,17 @@ old_cd=$(pwd) cd $(dirname "$0") # 启动参数声明 -build_mode=debug +build_mode=RelWithDebInfo echo echo echo --------------------------------------------------------------- -echo check build param[debug/release] +echo check build param[Debug/Release/MinSizeRel/RelWithDebInfo] echo --------------------------------------------------------------- # 编译参数检查 -build_mode=$(echo $1 | tr '[:upper:]' '[:lower:]') -if [[ $build_mode != "release" && $build_mode != "debug" ]]; then +build_mode=$(echo $1) +if [[ $build_mode != "Release" && $build_mode != "Debug" && $build_mode != "MinSizeRel" && $build_mode != "RelWithDebInfo" ]]; then echo "error: unkonow build mode -- $1" exit 1 fi @@ -39,45 +39,35 @@ fi # 提示 echo current build mode: $build_mode -# 环境变量设置 -export PATH=$qt_clang_path/bin:$PATH - echo echo echo --------------------------------------------------------------- -echo begin qmake build +echo begin cmake build echo --------------------------------------------------------------- # 删除输出目录 -output_path=$script_path../../output/mac/$build_mode +output_path=$script_path../../output if [ -d "$output_path" ]; then rm -rf $output_path fi -# 删除临时目录 -temp_path=$script_path/../temp -if [ -d "$temp_path" ]; then - rm -rf $temp_path +# 删除编译目录 +build_path=$script_path/../build_temp +if [ -d "$build_path" ]; then + rm -rf $build_path fi -mkdir $temp_path -cd $temp_path +mkdir $build_path +cd $build_path -qmake_params="-spec macx-clang" -if [ $build_mode == "debug" ]; then - qmake_params="$qmake_params CONFIG+=debug CONFIG+=x86_64 CONFIG+=qml_debug" -else - qmake_params="$qmake_params CONFIG+=x86_64 CONFIG+=qtquickcompiler" -fi - -# qmake ../../all.pro -spec macx-clang CONFIG+=debug CONFIG+=x86_64 CONFIG+=qml_debug -qmake ../../all.pro $qmake_params +cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode -G Xcode" +cmake $cmake_params ../.. if [ $? -ne 0 ] ;then - echo "qmake failed" + echo "cmake failed" exit 1 fi -make -j8 +cmake --build . --config $build_mode -j8 if [ $? -ne 0 ] ;then - echo "make failed" + echo "cmake build failed" exit 1 fi diff --git a/ci/mac/publish_for_mac.sh b/ci/mac/publish_for_mac.sh index deb4ef1..c16b426 100755 --- a/ci/mac/publish_for_mac.sh +++ b/ci/mac/publish_for_mac.sh @@ -30,7 +30,7 @@ keymap_path=$script_path/../../keymap # config_path=$script_path/../../config publish_path=$script_path/$publish_dir -release_path=$script_path/../../output/mac/release +release_path=$script_path/../../output/x64/RelWithDebInfo export PATH=$qt_clang_path/bin:$PATH diff --git a/ci/win/build_for_win.bat b/ci/win/build_for_win.bat index edf2258..0e7e584 100644 --- a/ci/win/build_for_win.bat +++ b/ci/win/build_for_win.bat @@ -7,12 +7,11 @@ echo check ENV echo --------------------------------------------------------------- :: 从环境变量获取必要参数 -:: example: D:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\VC\Auxiliary\Build\vcvarsall.bat -set vcvarsall="%ENV_VCVARSALL%" -:: example: D:\Qt\Qt5.12.5\5.12.5 -set qt_msvc_path="%ENV_QT_PATH%" +:: example: D:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvarsall.bat +:: set vcvarsall="%ENV_VCVARSALL%" -echo ENV_VCVARSALL %ENV_VCVARSALL% +:: example: D:\Qt\Qt5.12.5\5.12.5 +:: echo ENV_VCVARSALL %ENV_VCVARSALL% echo ENV_QT_PATH %ENV_QT_PATH% :: 获取脚本绝对路径 @@ -23,96 +22,82 @@ cd /d %~dp0 :: 启动参数声明 set cpu_mode=x86 -set build_mode=debug +set build_mode=RelWithDebInfo set errno=1 echo= echo= echo --------------------------------------------------------------- -echo check build param[debug/release x86/x64] +echo check build param[Debug/Release/MinSizeRel/RelWithDebInfo] echo --------------------------------------------------------------- -:: 编译参数检查 /i忽略大小写 -if /i "%1"=="debug" ( - set build_mode=debug +:: 编译参数检查 +if "%1"=="Debug" ( goto build_mode_ok ) -if /i "%1"=="release" ( - set build_mode=release +if "%1"=="Release" ( + goto build_mode_ok +) +if "%1"=="MinSizeRel" ( + goto build_mode_ok +) +if "%1"=="RelWithDebInfo" ( goto build_mode_ok ) echo error: unkonow build mode -- %1 goto return :build_mode_ok +set build_mode=%1 +set cmake_vs_build_mode=Win32 +set qt_cmake_path=%ENV_QT_PATH%\msvc2019\lib\cmake\Qt5 + if /i "%2"=="x86" ( set cpu_mode=x86 + set cmake_vs_build_mode=Win32 + set qt_cmake_path=%ENV_QT_PATH%\msvc2019\lib\cmake\Qt5 ) if /i "%2"=="x64" ( set cpu_mode=x64 + set cmake_vs_build_mode=x64 + set qt_cmake_path=%ENV_QT_PATH%\msvc2019_64\lib\cmake\Qt5 ) :: 提示 echo current build mode: %build_mode% %cpu_mode% - -:: 环境变量设置 -if /i %cpu_mode% == x86 ( - set qt_msvc_path=%qt_msvc_path%\msvc2017\bin -) else ( - set qt_msvc_path=%qt_msvc_path%\msvc2017_64\bin -) -set PATH=%qt_msvc_path%;%PATH% - -:: 注册vc环境 -if /i %cpu_mode% == x86 ( - call %vcvarsall% %cpu_mode% -) else ( - call %vcvarsall% %cpu_mode% -) - -if not %errorlevel%==0 ( - echo "vcvarsall not find" - goto return -) +echo qt cmake path: %qt_cmake_path% echo= echo= echo --------------------------------------------------------------- -echo begin qmake build +echo begin cmake build echo --------------------------------------------------------------- :: 删除输出目录 -set output_path=%script_path%..\..\output\win\%cpu_mode%\%build_mode% +set output_path=%script_path%..\..\output if exist %output_path% ( rmdir /q /s %output_path% ) :: 删除临时目录 -set temp_path=%script_path%..\temp +set temp_path=%script_path%..\build_temp if exist %temp_path% ( rmdir /q /s %temp_path% ) md %temp_path% cd %temp_path% -set qmake_params=-spec win32-msvc -if /i %build_mode% == debug ( - set qmake_params=%qmake_params% "CONFIG+=debug" "CONFIG+=qml_debug" -) else ( - set qmake_params=%qmake_params% "CONFIG+=qtquickcompiler" -) +set cmake_params=-DCMAKE_PREFIX_PATH=%qt_cmake_path% -DCMAKE_BUILD_TYPE=%build_mode% -G "Visual Studio 16 2019" -A %cmake_vs_build_mode% +echo cmake params: %cmake_params% -:: qmake ../../all.pro -spec win32-msvc "CONFIG+=debug" "CONFIG+=qml_debug" -qmake ../../all.pro %qmake_params% +cmake %cmake_params% ../.. if not %errorlevel%==0 ( - echo "qmake failed" + echo "cmake failed" goto return ) -:: nmake -:: jom是qt的多进程nmake工具 -..\win\jom -j8 +cmake --build . --config %build_mode% -j8 if not %errorlevel%==0 ( - echo "nmake failed" + echo "cmake build failed" goto return ) diff --git a/ci/win/publish_for_win.bat b/ci/win/publish_for_win.bat index 5b56112..b9dfcdd 100644 --- a/ci/win/publish_for_win.bat +++ b/ci/win/publish_for_win.bat @@ -7,7 +7,7 @@ echo check ENV echo --------------------------------------------------------------- :: 从环境变量获取必要参数 -:: example: D:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\VC\Auxiliary\Build\vcvarsall.bat +:: example: D:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvarsall.bat set vcvarsall="%ENV_VCVARSALL%" :: 例如 d:\a\QtScrcpy\Qt\5.12.7 set qt_msvc_path="%ENV_QT_PATH%" @@ -47,12 +47,12 @@ set config_path=%script_path%..\..\config if /i %cpu_mode% == x86 ( set publish_path=%script_path%%publish_dir%\ - set release_path=%script_path%..\..\output\win\x86\release - set qt_msvc_path=%qt_msvc_path%\msvc2017\bin + set release_path=%script_path%..\..\output\x86\RelWithDebInfo + set qt_msvc_path=%qt_msvc_path%\msvc2019\bin ) else ( set publish_path=%script_path%%publish_dir%\ - set release_path=%script_path%..\..\output\win\x64\release - set qt_msvc_path=%qt_msvc_path%\msvc2017_64\bin + set release_path=%script_path%..\..\output\x64\RelWithDebInfo + set qt_msvc_path=%qt_msvc_path%\msvc2019_64\bin ) set PATH=%qt_msvc_path%;%PATH% diff --git a/config/config.ini b/config/config.ini index 7f61934..3a73dcd 100644 --- a/config/config.ini +++ b/config/config.ini @@ -15,13 +15,13 @@ ServerVersion=1.21 ServerPath=/data/local/tmp/scrcpy-server.jar # 自定义adb路径,例如D:/android/tools/adb.exe AdbPath= -# 编码选项 "-"表示默认 +# 编码选项 ""表示默认 # 例如 CodecOptions="profile=1,level=2" # 更多编码选项参考 https://d.android.com/reference/android/media/MediaFormat CodecOptions="" -# 指定编码器名称,必须是H.264编码器 +# 指定编码器名称(必须是H.264编码器),""表示默认 # 例如 CodecName="OMX.qcom.video.encoder.avc" -CodecName="OMX.qcom.video.encoder.avc" +CodecName="" # Set the log level (debug, info, warn, error) LogLevel=info From f2641816d1276305fa15b22ebba9391680a34e8e Mon Sep 17 00:00:00 2001 From: Barry <870709864@qq.com> Date: Sun, 9 Jan 2022 15:49:51 +0800 Subject: [PATCH 5/6] feat: update server --- server/.gitignore | 8 + server/build.gradle | 10 +- server/build_without_gradle.sh | 88 +++++++ server/meson.build | 25 ++ .../java/com/genymobile/scrcpy/CleanUp.java | 154 ++++++++++-- .../com/genymobile/scrcpy/CodecOption.java | 2 +- .../java/com/genymobile/scrcpy/Command.java | 33 +++ .../com/genymobile/scrcpy/ControlMessage.java | 62 +++-- .../scrcpy/ControlMessageReader.java | 60 +++-- .../com/genymobile/scrcpy/Controller.java | 136 ++++++++--- .../java/com/genymobile/scrcpy/Device.java | 149 ++++++++---- .../com/genymobile/scrcpy/DeviceMessage.java | 15 ++ .../scrcpy/DeviceMessageSender.java | 24 +- .../scrcpy/DeviceMessageWriter.java | 14 +- .../scrcpy/InvalidEncoderException.java | 23 ++ .../main/java/com/genymobile/scrcpy/Ln.java | 20 +- .../java/com/genymobile/scrcpy/Options.java | 53 ++++- .../com/genymobile/scrcpy/ScreenEncoder.java | 56 +++-- .../com/genymobile/scrcpy/ScreenInfo.java | 6 + .../java/com/genymobile/scrcpy/Server.java | 222 ++++++++++++------ .../java/com/genymobile/scrcpy/Settings.java | 84 +++++++ .../genymobile/scrcpy/SettingsException.java | 11 + .../com/genymobile/scrcpy/Workarounds.java | 1 + .../scrcpy/wrappers/ActivityManager.java | 6 +- .../scrcpy/wrappers/ContentProvider.java | 96 +++++--- .../scrcpy/wrappers/ServiceManager.java | 9 +- .../scrcpy/wrappers/StatusBarManager.java | 46 +++- .../scrcpy/ControlMessageReaderTest.java | 57 +++-- .../scrcpy/DeviceMessageWriterTest.java | 22 +- 29 files changed, 1204 insertions(+), 288 deletions(-) create mode 100644 server/.gitignore create mode 100644 server/build_without_gradle.sh create mode 100644 server/meson.build create mode 100644 server/src/main/java/com/genymobile/scrcpy/Command.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/Settings.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/SettingsException.java diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..0df7064 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/ +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/server/build.gradle b/server/build.gradle index c8ff85d..1f939a1 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,13 +1,13 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 29 + compileSdkVersion 31 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 29 - versionCode 16 - versionName "1.14" + targetSdkVersion 31 + versionCode 12100 + versionName "1.21" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -20,7 +20,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.1' } apply from: "$project.rootDir/config/android-checkstyle.gradle" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh new file mode 100644 index 0000000..0f86c29 --- /dev/null +++ b/server/build_without_gradle.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# +# This script generates the scrcpy binary "manually" (without gradle). +# +# Adapt Android platform and build tools versions (via ANDROID_PLATFORM and +# ANDROID_BUILD_TOOLS environment variables). +# +# Then execute: +# +# BUILD_DIR=my_build_dir ./build_without_gradle.sh + +set -e + +SCRCPY_DEBUG=false +SCRCPY_VERSION_NAME=1.21 + +PLATFORM_VERSION=31 +PLATFORM=${ANDROID_PLATFORM:-$PLATFORM_VERSION} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0} + +BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" +CLASSES_DIR="$BUILD_DIR/classes" +SERVER_DIR=$(dirname "$0") +SERVER_BINARY=scrcpy-server +ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" + +echo "Platform: android-$PLATFORM" +echo "Build-tools: $BUILD_TOOLS" +echo "Build dir: $BUILD_DIR" + +rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex +mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy" + +<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java" +package com.genymobile.scrcpy; + +public final class BuildConfig { + public static final boolean DEBUG = $SCRCPY_DEBUG; + public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME"; +} +EOF + +echo "Generating java from aidl..." +cd "$SERVER_DIR/src/main/aidl" +"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \ + android/view/IRotationWatcher.aidl +"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \ + android/content/IOnPrimaryClipChangedListener.aidl + +echo "Compiling java sources..." +cd ../java +javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \ + -source 1.8 -target 1.8 \ + com/genymobile/scrcpy/*.java \ + com/genymobile/scrcpy/wrappers/*.java + +echo "Dexing..." +cd "$CLASSES_DIR" + +if [[ $PLATFORM_VERSION -lt 31 ]] +then + # use dx + "$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ + --output "$BUILD_DIR/classes.dex" \ + android/view/*.class \ + android/content/*.class \ + com/genymobile/scrcpy/*.class \ + com/genymobile/scrcpy/wrappers/*.class + + echo "Archiving..." + cd "$BUILD_DIR" + jar cvf "$SERVER_BINARY" classes.dex + rm -rf classes.dex classes +else + # use d8 + "$ANDROID_HOME/build-tools/$BUILD_TOOLS/d8" --classpath "$ANDROID_JAR" \ + --output "$BUILD_DIR/classes.zip" \ + android/view/*.class \ + android/content/*.class \ + com/genymobile/scrcpy/*.class \ + com/genymobile/scrcpy/wrappers/*.class + + cd "$BUILD_DIR" + mv classes.zip "$SERVER_BINARY" + rm -rf classes +fi + +echo "Server generated in $BUILD_DIR/$SERVER_BINARY" diff --git a/server/meson.build b/server/meson.build new file mode 100644 index 0000000..984daf3 --- /dev/null +++ b/server/meson.build @@ -0,0 +1,25 @@ +# It may be useful to use a prebuilt server, so that no Android SDK is required +# to build. If the 'prebuilt_server' option is set, just copy the file as is. +prebuilt_server = get_option('prebuilt_server') +if prebuilt_server == '' + custom_target('scrcpy-server', + # gradle is responsible for tracking source changes + build_by_default: true, + build_always_stale: true, + output: 'scrcpy-server', + command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], + console: true, + install: true, + install_dir: 'share/scrcpy') +else + if not prebuilt_server.startswith('/') + # relative path needs some trick + prebuilt_server = meson.source_root() + '/' + prebuilt_server + endif + custom_target('scrcpy-server-prebuilt', + input: prebuilt_server, + output: 'scrcpy-server', + command: ['cp', '@INPUT@', '@OUTPUT@'], + install: true, + install_dir: 'share/scrcpy') +endif diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 7455563..319a957 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -1,8 +1,11 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Base64; + import java.io.File; import java.io.IOException; @@ -15,22 +18,123 @@ public final class CleanUp { public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; + // A simple struct to be passed from the main process to the cleanup process + public static class Config implements Parcelable { + + public static final Creator CREATOR = new Creator() { + @Override + public Config createFromParcel(Parcel in) { + return new Config(in); + } + + @Override + public Config[] newArray(int size) { + return new Config[size]; + } + }; + + private static final int FLAG_DISABLE_SHOW_TOUCHES = 1; + private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2; + private static final int FLAG_POWER_OFF_SCREEN = 4; + + private int displayId; + + // Restore the value (between 0 and 7), -1 to not restore + // + private int restoreStayOn = -1; + + private boolean disableShowTouches; + private boolean restoreNormalPowerMode; + private boolean powerOffScreen; + + public Config() { + // Default constructor, the fields are initialized by CleanUp.configure() + } + + protected Config(Parcel in) { + displayId = in.readInt(); + restoreStayOn = in.readInt(); + byte options = in.readByte(); + disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0; + restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0; + powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(displayId); + dest.writeInt(restoreStayOn); + byte options = 0; + if (disableShowTouches) { + options |= FLAG_DISABLE_SHOW_TOUCHES; + } + if (restoreNormalPowerMode) { + options |= FLAG_RESTORE_NORMAL_POWER_MODE; + } + if (powerOffScreen) { + options |= FLAG_POWER_OFF_SCREEN; + } + dest.writeByte(options); + } + + private boolean hasWork() { + return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen; + } + + @Override + public int describeContents() { + return 0; + } + + byte[] serialize() { + Parcel parcel = Parcel.obtain(); + writeToParcel(parcel, 0); + byte[] bytes = parcel.marshall(); + parcel.recycle(); + return bytes; + } + + static Config deserialize(byte[] bytes) { + Parcel parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + return CREATOR.createFromParcel(parcel); + } + + static Config fromBase64(String base64) { + byte[] bytes = Base64.decode(base64, Base64.NO_WRAP); + return deserialize(bytes); + } + + String toBase64() { + byte[] bytes = serialize(); + return Base64.encodeToString(bytes, Base64.NO_WRAP); + } + } + private CleanUp() { // not instantiable } - public static void configure(boolean disableShowTouches, int restoreStayOn) throws IOException { - boolean needProcess = disableShowTouches || restoreStayOn != -1; - if (needProcess) { - startProcess(disableShowTouches, restoreStayOn); + public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen) + throws IOException { + Config config = new Config(); + config.displayId = displayId; + config.disableShowTouches = disableShowTouches; + config.restoreStayOn = restoreStayOn; + config.restoreNormalPowerMode = restoreNormalPowerMode; + config.powerOffScreen = powerOffScreen; + + if (config.hasWork()) { + startProcess(config); } 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)}; + private static void startProcess(Config config) throws IOException { + String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()}; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", SERVER_PATH); @@ -57,21 +161,37 @@ public final class CleanUp { Ln.i("Cleaning up"); - boolean disableShowTouches = Boolean.parseBoolean(args[0]); - int restoreStayOn = Integer.parseInt(args[1]); + Config config = Config.fromBase64(args[0]); - if (disableShowTouches || restoreStayOn != -1) { + if (config.disableShowTouches || config.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"); + Settings settings = new Settings(serviceManager); + if (config.disableShowTouches) { + Ln.i("Disabling \"show touches\""); + try { + settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0"); + } catch (SettingsException e) { + Ln.e("Could not restore \"show_touches\"", e); } - if (restoreStayOn != -1) { - Ln.i("Restoring \"stay awake\""); - settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn)); + } + if (config.restoreStayOn != -1) { + Ln.i("Restoring \"stay awake\""); + try { + settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); + } catch (SettingsException e) { + Ln.e("Could not restore \"stay_on_while_plugged_in\"", e); } } } + + if (Device.isScreenOn()) { + if (config.powerOffScreen) { + Ln.i("Power off screen"); + Device.powerOffScreen(config.displayId); + } else if (config.restoreNormalPowerMode) { + Ln.i("Restoring normal power mode"); + Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); + } + } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java index 1897bda..12f2a88 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java +++ b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java @@ -21,7 +21,7 @@ public class CodecOption { } public static List parse(String codecOptions) { - if ("-".equals(codecOptions)) { + if (codecOptions.isEmpty()) { return null; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Command.java b/server/src/main/java/com/genymobile/scrcpy/Command.java new file mode 100644 index 0000000..0ef976a --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Command.java @@ -0,0 +1,33 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Scanner; + +public final class Command { + private Command() { + // not instantiable + } + + public static void exec(String... cmd) throws IOException, InterruptedException { + Process process = Runtime.getRuntime().exec(cmd); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); + } + } + + public static String execReadLine(String... cmd) throws IOException, InterruptedException { + String result = null; + Process process = Runtime.getRuntime().exec(cmd); + Scanner scanner = new Scanner(process.getInputStream()); + if (scanner.hasNextLine()) { + result = scanner.nextLine(); + } + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); + } + return result; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 7d0ab7a..63ba0fa 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -11,13 +11,18 @@ public final class ControlMessage { public static final int TYPE_INJECT_SCROLL_EVENT = 3; public static final int TYPE_BACK_OR_SCREEN_ON = 4; public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; - public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; - public static final int TYPE_GET_CLIPBOARD = 7; - public static final int TYPE_SET_CLIPBOARD = 8; - public static final int TYPE_SET_SCREEN_POWER_MODE = 9; - public static final int TYPE_ROTATE_DEVICE = 10; + public static final int TYPE_EXPAND_SETTINGS_PANEL = 6; + public static final int TYPE_COLLAPSE_PANELS = 7; + public static final int TYPE_GET_CLIPBOARD = 8; + public static final int TYPE_SET_CLIPBOARD = 9; + public static final int TYPE_SET_SCREEN_POWER_MODE = 10; + public static final int TYPE_ROTATE_DEVICE = 11; - public static final int FLAGS_PASTE = 1; + public static final long SEQUENCE_INVALID = 0; + + public static final int COPY_KEY_NONE = 0; + public static final int COPY_KEY_COPY = 1; + public static final int COPY_KEY_CUT = 2; private int type; private String text; @@ -30,16 +35,20 @@ public final class ControlMessage { private Position position; private int hScroll; private int vScroll; - private int flags; + private int copyKey; + private boolean paste; + private int repeat; + private long sequence; private ControlMessage() { } - public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) { + public static ControlMessage createInjectKeycode(int action, int keycode, int repeat, int metaState) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_KEYCODE; msg.action = action; msg.keycode = keycode; + msg.repeat = repeat; msg.metaState = metaState; return msg; } @@ -71,13 +80,26 @@ public final class ControlMessage { return msg; } - public static ControlMessage createSetClipboard(String text, boolean paste) { + public static ControlMessage createBackOrScreenOn(int action) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_BACK_OR_SCREEN_ON; + msg.action = action; + return msg; + } + + public static ControlMessage createGetClipboard(int copyKey) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_GET_CLIPBOARD; + msg.copyKey = copyKey; + return msg; + } + + public static ControlMessage createSetClipboard(long sequence, String text, boolean paste) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_CLIPBOARD; + msg.sequence = sequence; msg.text = text; - if (paste) { - msg.flags = FLAGS_PASTE; - } + msg.paste = paste; return msg; } @@ -141,7 +163,19 @@ public final class ControlMessage { return vScroll; } - public int getFlags() { - return flags; + public int getCopyKey() { + return copyKey; + } + + public boolean getPaste() { + return paste; + } + + public int getRepeat() { + return repeat; + } + + public long getSequence() { + return sequence; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index fbf49a6..f09ed26 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -8,20 +8,21 @@ import java.nio.charset.StandardCharsets; public class ControlMessageReader { - static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; + static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13; static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; + static final int BACK_OR_SCREEN_ON_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; - static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; + static final int GET_CLIPBOARD_LENGTH = 1; + static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9; - public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4092; // 4096 - 1 (type) - 1 (parse flag) - 2 (length) + private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k + + public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 14; // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes 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 byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); - private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH]; public ControlMessageReader() { // invariant: the buffer is always in "get" mode @@ -67,16 +68,21 @@ public class ControlMessageReader { case ControlMessage.TYPE_INJECT_SCROLL_EVENT: msg = parseInjectScrollEvent(); break; + case ControlMessage.TYPE_BACK_OR_SCREEN_ON: + msg = parseBackOrScreenOnEvent(); + break; + case ControlMessage.TYPE_GET_CLIPBOARD: + msg = parseGetClipboard(); + break; case ControlMessage.TYPE_SET_CLIPBOARD: msg = parseSetClipboard(); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: msg = parseSetScreenPowerMode(); break; - case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: - case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: - case ControlMessage.TYPE_GET_CLIPBOARD: + case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: + case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_ROTATE_DEVICE: msg = ControlMessage.createEmpty(type); break; @@ -99,20 +105,23 @@ public class ControlMessageReader { } int action = toUnsigned(buffer.get()); int keycode = buffer.getInt(); + int repeat = buffer.getInt(); int metaState = buffer.getInt(); - return ControlMessage.createInjectKeycode(action, keycode, metaState); + return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState); } private String parseString() { - if (buffer.remaining() < 2) { + if (buffer.remaining() < 4) { return null; } - int len = toUnsigned(buffer.getShort()); + int len = buffer.getInt(); if (buffer.remaining() < len) { return null; } - buffer.get(textBuffer, 0, len); - return new String(textBuffer, 0, len, StandardCharsets.UTF_8); + int position = buffer.position(); + // Move the buffer position to consume the text + buffer.position(position + len); + return new String(rawBuffer, position, len, StandardCharsets.UTF_8); } private ControlMessage parseInjectText() { @@ -148,16 +157,33 @@ public class ControlMessageReader { return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll); } + private ControlMessage parseBackOrScreenOnEvent() { + if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) { + return null; + } + int action = toUnsigned(buffer.get()); + return ControlMessage.createBackOrScreenOn(action); + } + + private ControlMessage parseGetClipboard() { + if (buffer.remaining() < GET_CLIPBOARD_LENGTH) { + return null; + } + int copyKey = toUnsigned(buffer.get()); + return ControlMessage.createGetClipboard(copyKey); + } + private ControlMessage parseSetClipboard() { if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { return null; } - boolean parse = buffer.get() != 0; + long sequence = buffer.getLong(); + boolean paste = buffer.get() != 0; String text = parseString(); if (text == null) { return null; } - return ControlMessage.createSetClipboard(text, parse); + return ControlMessage.createSetClipboard(sequence, text, paste); } private ControlMessage parseSetScreenPowerMode() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 960c6a6..9246004 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -8,14 +8,20 @@ import android.view.KeyEvent; import android.view.MotionEvent; import java.io.IOException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; public class Controller { - private static final int DEVICE_ID_VIRTUAL = -1; + private static final int DEFAULT_DEVICE_ID = 0; + + private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); private final Device device; private final DesktopConnection connection; private final DeviceMessageSender sender; + private final boolean clipboardAutosync; private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); @@ -24,9 +30,12 @@ public class Controller { private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; - public Controller(Device device, DesktopConnection connection) { + private boolean keepPowerModeOff; + + public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync) { this.device = device; this.connection = connection; + this.clipboardAutosync = clipboardAutosync; initPointers(); sender = new DeviceMessageSender(connection); } @@ -38,7 +47,7 @@ public class Controller { MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); coords.orientation = 0; - coords.size = 1; + coords.size = 0; pointerProperties[i] = props; pointerCoords[i] = coords; @@ -47,8 +56,8 @@ public class Controller { public void control() throws IOException { // on start, power on the device - if (!device.isScreenOn()) { - device.injectKeycode(KeyEvent.KEYCODE_POWER); + if (!Device.isScreenOn()) { + device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); // dirty hack // After POWER is injected, the device is powered on asynchronously. @@ -74,7 +83,7 @@ public class Controller { switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: if (device.supportsInputEvents()) { - injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState()); } break; case ControlMessage.TYPE_INJECT_TEXT: @@ -94,44 +103,47 @@ public class Controller { break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: if (device.supportsInputEvents()) { - pressBackOrTurnScreenOn(); + pressBackOrTurnScreenOn(msg.getAction()); } break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: - device.expandNotificationPanel(); + Device.expandNotificationPanel(); break; - case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: - device.collapsePanels(); + case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: + Device.expandSettingsPanel(); + break; + case ControlMessage.TYPE_COLLAPSE_PANELS: + Device.collapsePanels(); break; case ControlMessage.TYPE_GET_CLIPBOARD: - String clipboardText = device.getClipboardText(); - if (clipboardText != null) { - sender.pushClipboardText(clipboardText); - } + getClipboard(msg.getCopyKey()); break; case ControlMessage.TYPE_SET_CLIPBOARD: - boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0; - setClipboard(msg.getText(), paste); + setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: if (device.supportsInputEvents()) { int mode = msg.getAction(); - boolean setPowerModeOk = device.setScreenPowerMode(mode); + boolean setPowerModeOk = Device.setScreenPowerMode(mode); if (setPowerModeOk) { + keepPowerModeOff = mode == Device.POWER_MODE_OFF; Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); } } break; case ControlMessage.TYPE_ROTATE_DEVICE: - device.rotateDevice(); + Device.rotateDevice(); break; default: // do nothing } } - private boolean injectKeycode(int action, int keycode, int metaState) { - return device.injectKeyEvent(action, keycode, 0, metaState); + private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { + if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { + schedulePowerModeOff(); + } + return device.injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); } private boolean injectChar(char c) { @@ -142,7 +154,7 @@ public class Controller { return false; } for (KeyEvent event : events) { - if (!device.injectEvent(event)) { + if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -166,7 +178,7 @@ public class Controller { Point point = device.getPhysicalPoint(position); if (point == null) { - // ignore event + Ln.w("Ignore touch event, it was generated for a different device size"); return false; } @@ -195,10 +207,18 @@ public class Controller { } } + // Right-click and middle-click only work if the source is a mouse + boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0; + int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN; + if (source != InputDevice.SOURCE_MOUSE) { + // Buttons must not be set for touch events + buttons = 0; + } + MotionEvent event = MotionEvent - .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, - InputDevice.SOURCE_TOUCHSCREEN, 0); - return device.injectEvent(event); + .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, + 0); + return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } private boolean injectScroll(Position position, int hScroll, int vScroll) { @@ -219,17 +239,62 @@ public class Controller { coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); MotionEvent event = MotionEvent - .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0, - InputDevice.SOURCE_TOUCHSCREEN, 0); - return device.injectEvent(event); + .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEFAULT_DEVICE_ID, 0, + InputDevice.SOURCE_MOUSE, 0); + return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } - private boolean pressBackOrTurnScreenOn() { - int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; - return device.injectKeycode(keycode); + /** + * Schedule a call to set power mode to off after a small delay. + */ + private static void schedulePowerModeOff() { + EXECUTOR.schedule(new Runnable() { + @Override + public void run() { + Ln.i("Forcing screen off"); + Device.setScreenPowerMode(Device.POWER_MODE_OFF); + } + }, 200, TimeUnit.MILLISECONDS); } - private boolean setClipboard(String text, boolean paste) { + private boolean pressBackOrTurnScreenOn(int action) { + if (Device.isScreenOn()) { + return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); + } + + // Screen is off + // Only press POWER on ACTION_DOWN + if (action != KeyEvent.ACTION_DOWN) { + // do nothing, + return true; + } + + if (keepPowerModeOff) { + schedulePowerModeOff(); + } + return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); + } + + private void getClipboard(int copyKey) { + // On Android >= 7, press the COPY or CUT key if requested + if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT; + // Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one + device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); + } + + // If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in + // particular when COPY or CUT are injected, so it should not be synchronized twice. On Android < 7, do not synchronize at all rather than + // copying an old clipboard content. + if (!clipboardAutosync) { + String clipboardText = Device.getClipboardText(); + if (clipboardText != null) { + sender.pushClipboardText(clipboardText); + } + } + } + + private boolean setClipboard(String text, boolean paste, long sequence) { boolean ok = device.setClipboardText(text); if (ok) { Ln.i("Device clipboard set"); @@ -237,7 +302,12 @@ public class Controller { // 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); + device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); + } + + if (sequence != ControlMessage.SEQUENCE_INVALID) { + // Acknowledgement requested + sender.pushAckClipboard(sequence); } return ok; diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 349486c..ba833a0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,6 +1,6 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.ContentProvider; +import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; @@ -24,6 +24,16 @@ public final class Device { public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; + public static final int INJECT_MODE_ASYNC = InputManager.INJECT_INPUT_EVENT_MODE_ASYNC; + public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT; + public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; + + public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; + public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; + + private static final ServiceManager SERVICE_MANAGER = new ServiceManager(); + private static final Settings SETTINGS = new Settings(SERVICE_MANAGER); + public interface RotationListener { void onRotationChanged(int rotation); } @@ -32,8 +42,6 @@ public final class Device { void onClipboardTextChanged(String text); } - private final ServiceManager serviceManager = new ServiceManager(); - private ScreenInfo screenInfo; private RotationListener rotationListener; private ClipboardListener clipboardListener; @@ -53,18 +61,18 @@ public final class Device { public Device(Options options) { displayId = options.getDisplayId(); - DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId); + DisplayInfo displayInfo = SERVICE_MANAGER.getDisplayManager().getDisplayInfo(displayId); if (displayInfo == null) { - int[] displayIds = serviceManager.getDisplayManager().getDisplayIds(); + int[] displayIds = SERVICE_MANAGER.getDisplayManager().getDisplayIds(); throw new InvalidDisplayIdException(displayId, displayIds); } int displayInfoFlags = displayInfo.getFlags(); - screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); + screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockVideoOrientation()); layerStack = displayInfo.getLayerStack(); - serviceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { + SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) { synchronized (Device.this) { @@ -78,25 +86,30 @@ 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 (options.getControl() && options.getClipboardAutosync()) { + // If control and autosync are enabled, synchronize Android clipboard to the computer automatically + ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); + if (clipboardManager != null) { + clipboardManager.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); + } } } } - } - }); + }); + } else { + Ln.w("No clipboard manager, copy-paste between device and computer will not work"); + } } if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { @@ -147,12 +160,16 @@ public final class Device { return Build.MODEL; } + public static boolean supportsInputEvents(int displayId) { + return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + } + public boolean supportsInputEvents() { return supportsInputEvents; } - public boolean injectEvent(InputEvent inputEvent, int mode) { - if (!supportsInputEvents()) { + public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) { + if (!supportsInputEvents(displayId)) { throw new AssertionError("Could not inject input event if !supportsInputEvents()"); } @@ -160,26 +177,35 @@ public final class Device { return false; } - return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); + return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, injectMode); } - public boolean injectEvent(InputEvent event) { - return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + public boolean injectEvent(InputEvent event, int injectMode) { + return injectEvent(event, displayId, injectMode); } - public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { + public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) { 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); + return injectEvent(event, displayId, injectMode); } - public boolean injectKeycode(int keyCode) { - return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); + public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { + return injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); } - public boolean isScreenOn() { - return serviceManager.getPowerManager().isScreenOn(); + public static boolean pressReleaseKeycode(int keyCode, int displayId, int injectMode) { + return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId, injectMode) + && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode); + } + + public boolean pressReleaseKeycode(int keyCode, int injectMode) { + return pressReleaseKeycode(keyCode, displayId, injectMode); + } + + public static boolean isScreenOn() { + return SERVICE_MANAGER.getPowerManager().isScreenOn(); } public synchronized void setRotationListener(RotationListener rotationListener) { @@ -190,16 +216,24 @@ public final class Device { this.clipboardListener = clipboardListener; } - public void expandNotificationPanel() { - serviceManager.getStatusBarManager().expandNotificationsPanel(); + public static void expandNotificationPanel() { + SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel(); } - public void collapsePanels() { - serviceManager.getStatusBarManager().collapsePanels(); + public static void expandSettingsPanel() { + SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel(); } - public String getClipboardText() { - CharSequence s = serviceManager.getClipboardManager().getText(); + public static void collapsePanels() { + SERVICE_MANAGER.getStatusBarManager().collapsePanels(); + } + + public static String getClipboardText() { + ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); + if (clipboardManager == null) { + return null; + } + CharSequence s = clipboardManager.getText(); if (s == null) { return null; } @@ -207,16 +241,30 @@ public final class Device { } public boolean setClipboardText(String text) { + ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); + if (clipboardManager == null) { + return false; + } + + String currentClipboard = getClipboardText(); + if (currentClipboard != null && currentClipboard.equals(text)) { + // The clipboard already contains the requested text. + // Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause + // the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this + // problem, do not explicitly set the clipboard text if it already contains the expected content. + return false; + } + isSettingClipboard.set(true); - boolean ok = serviceManager.getClipboardManager().setText(text); + boolean ok = clipboardManager.setText(text); isSettingClipboard.set(false); return ok; } /** - * @param mode one of the {@code SCREEN_POWER_MODE_*} constants + * @param mode one of the {@code POWER_MODE_*} constants */ - public boolean setScreenPowerMode(int mode) { + public static boolean setScreenPowerMode(int mode) { IBinder d = SurfaceControl.getBuiltInDisplay(); if (d == null) { Ln.e("Could not get built-in display"); @@ -225,11 +273,18 @@ public final class Device { return SurfaceControl.setDisplayPowerMode(d, mode); } + public static boolean powerOffScreen(int displayId) { + if (!isScreenOn()) { + return true; + } + return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); + } + /** * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). */ - public void rotateDevice() { - WindowManager wm = serviceManager.getWindowManager(); + public static void rotateDevice() { + WindowManager wm = SERVICE_MANAGER.getWindowManager(); boolean accelerometerRotation = !wm.isRotationFrozen(); @@ -246,7 +301,7 @@ public final class Device { } } - public ContentProvider createSettingsProvider() { - return serviceManager.getActivityManager().createSettingsProvider(); + public static Settings getSettings() { + return SETTINGS; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java index c6eebd3..5b7c4de 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java @@ -3,9 +3,13 @@ package com.genymobile.scrcpy; public final class DeviceMessage { public static final int TYPE_CLIPBOARD = 0; + public static final int TYPE_ACK_CLIPBOARD = 1; + + public static final long SEQUENCE_INVALID = ControlMessage.SEQUENCE_INVALID; private int type; private String text; + private long sequence; private DeviceMessage() { } @@ -17,6 +21,13 @@ public final class DeviceMessage { return event; } + public static DeviceMessage createAckClipboard(long sequence) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_ACK_CLIPBOARD; + event.sequence = sequence; + return event; + } + public int getType() { return type; } @@ -24,4 +35,8 @@ public final class DeviceMessage { public String getText() { return text; } + + public long getSequence() { + return sequence; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index bbf4dd2..4ebccac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -8,6 +8,8 @@ public final class DeviceMessageSender { private String clipboardText; + private long ack; + public DeviceMessageSender(DesktopConnection connection) { this.connection = connection; } @@ -17,18 +19,34 @@ public final class DeviceMessageSender { notify(); } + public synchronized void pushAckClipboard(long sequence) { + ack = sequence; + notify(); + } + public void loop() throws IOException, InterruptedException { while (true) { String text; + long sequence; synchronized (this) { - while (clipboardText == null) { + while (ack == DeviceMessage.SEQUENCE_INVALID && clipboardText == null) { wait(); } text = clipboardText; clipboardText = null; + + sequence = ack; + ack = DeviceMessage.SEQUENCE_INVALID; + } + + if (sequence != DeviceMessage.SEQUENCE_INVALID) { + DeviceMessage event = DeviceMessage.createAckClipboard(sequence); + connection.sendDeviceMessage(event); + } + if (text != null) { + DeviceMessage event = DeviceMessage.createClipboard(text); + connection.sendDeviceMessage(event); } - DeviceMessage event = DeviceMessage.createClipboard(text); - connection.sendDeviceMessage(event); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index 6c7f363..bcd8d20 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -7,24 +7,28 @@ import java.nio.charset.StandardCharsets; public class DeviceMessageWriter { - public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; - private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3; + private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k + public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes - private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE]; + private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { buffer.clear(); - buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD); + buffer.put((byte) msg.getType()); switch (msg.getType()) { case DeviceMessage.TYPE_CLIPBOARD: String text = msg.getText(); byte[] raw = text.getBytes(StandardCharsets.UTF_8); int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); - buffer.putShort((short) len); + buffer.putInt(len); buffer.put(raw, 0, len); output.write(rawBuffer, 0, buffer.position()); break; + case DeviceMessage.TYPE_ACK_CLIPBOARD: + buffer.putLong(msg.getSequence()); + output.write(rawBuffer, 0, buffer.position()); + break; default: Ln.w("Unknown device message: " + msg.getType()); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java b/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java new file mode 100644 index 0000000..1efd298 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java @@ -0,0 +1,23 @@ +package com.genymobile.scrcpy; + +import android.media.MediaCodecInfo; + +public class InvalidEncoderException extends RuntimeException { + + private final String name; + private final MediaCodecInfo[] availableEncoders; + + public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) { + super("There is no encoder having name '" + name + '"'); + this.name = name; + this.availableEncoders = availableEncoders; + } + + public String getName() { + return name; + } + + public MediaCodecInfo[] getAvailableEncoders() { + return availableEncoders; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index c218fa0..c39fc62 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -12,7 +12,7 @@ public final class Ln { private static final String PREFIX = "[server] "; enum Level { - DEBUG, INFO, WARN, ERROR + VERBOSE, DEBUG, INFO, WARN, ERROR } private static Level threshold = Level.INFO; @@ -36,6 +36,13 @@ public final class Ln { return level.ordinal() >= threshold.ordinal(); } + public static void v(String message) { + if (isEnabled(Level.VERBOSE)) { + Log.v(TAG, message); + System.out.println(PREFIX + "VERBOSE: " + message); + } + } + public static void d(String message) { if (isEnabled(Level.DEBUG)) { Log.d(TAG, message); @@ -50,13 +57,20 @@ public final class Ln { } } - public static void w(String message) { + public static void w(String message, Throwable throwable) { if (isEnabled(Level.WARN)) { - Log.w(TAG, message); + Log.w(TAG, message, throwable); System.out.println(PREFIX + "WARN: " + message); + if (throwable != null) { + throwable.printStackTrace(); + } } } + public static void w(String message) { + w(message, null); + } + public static void e(String message, Throwable throwable) { if (isEnabled(Level.ERROR)) { Log.e(TAG, message, throwable); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 06312a3..1ac1717 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -2,20 +2,25 @@ package com.genymobile.scrcpy; import android.graphics.Rect; +import java.util.List; + public class Options { - private Ln.Level logLevel; + private Ln.Level logLevel = Ln.Level.DEBUG; private int maxSize; - private int bitRate; + private int bitRate = 8000000; private int maxFps; - private int lockedVideoOrientation; + private int lockVideoOrientation = -1; private boolean tunnelForward; private Rect crop; - private boolean sendFrameMeta; // send PTS so that the client may record properly - private boolean control; + private boolean sendFrameMeta = true; // send PTS so that the client may record properly + private boolean control = true; private int displayId; private boolean showTouches; private boolean stayAwake; - private String codecOptions; + private List codecOptions; + private String encoderName; + private boolean powerOffScreenOnClose; + private boolean clipboardAutosync = true; public Ln.Level getLogLevel() { return logLevel; @@ -49,12 +54,12 @@ public class Options { this.maxFps = maxFps; } - public int getLockedVideoOrientation() { - return lockedVideoOrientation; + public int getLockVideoOrientation() { + return lockVideoOrientation; } - public void setLockedVideoOrientation(int lockedVideoOrientation) { - this.lockedVideoOrientation = lockedVideoOrientation; + public void setLockVideoOrientation(int lockVideoOrientation) { + this.lockVideoOrientation = lockVideoOrientation; } public boolean isTunnelForward() { @@ -113,11 +118,35 @@ public class Options { this.stayAwake = stayAwake; } - public String getCodecOptions() { + public List getCodecOptions() { return codecOptions; } - public void setCodecOptions(String codecOptions) { + public void setCodecOptions(List codecOptions) { this.codecOptions = codecOptions; } + + public String getEncoderName() { + return encoderName; + } + + public void setEncoderName(String encoderName) { + this.encoderName = encoderName; + } + + public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) { + this.powerOffScreenOnClose = powerOffScreenOnClose; + } + + public boolean getPowerOffScreenOnClose() { + return this.powerOffScreenOnClose; + } + + public boolean getClipboardAutosync() { + return clipboardAutosync; + } + + public void setClipboardAutosync(boolean clipboardAutosync) { + this.clipboardAutosync = clipboardAutosync; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index d722388..f98c53d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -5,13 +5,17 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl; import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; +import android.media.MediaCodecList; 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.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -26,17 +30,19 @@ public class ScreenEncoder implements Device.RotationListener { private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); + private String encoderName; private List codecOptions; private int bitRate; private int maxFps; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; this.codecOptions = codecOptions; + this.encoderName = encoderName; } @Override @@ -50,17 +56,13 @@ public class ScreenEncoder implements Device.RotationListener { public void streamScreen(Device device, FileDescriptor fd) throws IOException { Workarounds.prepareMainLooper(); - - try { - internalStreamScreen(device, fd); - } catch (NullPointerException e) { - // Retry with workarounds enabled: - // - // - Ln.d("Applying workarounds to avoid NullPointerException"); + if (Build.BRAND.equalsIgnoreCase("meizu")) { + // + // Workarounds.fillAppInfo(); - internalStreamScreen(device, fd); } + + internalStreamScreen(device, fd); } private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { @@ -69,7 +71,7 @@ public class ScreenEncoder implements Device.RotationListener { boolean alive; try { do { - MediaCodec codec = createCodec(); + MediaCodec codec = createCodec(encoderName); IBinder display = createDisplay(); ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = screenInfo.getContentRect(); @@ -150,8 +152,30 @@ public class ScreenEncoder implements Device.RotationListener { IO.writeFully(fd, headerBuffer); } - private static MediaCodec createCodec() throws IOException { - return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); + private static MediaCodecInfo[] listEncoders() { + List result = new ArrayList<>(); + MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (MediaCodecInfo codecInfo : list.getCodecInfos()) { + if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) { + result.add(codecInfo); + } + } + return result.toArray(new MediaCodecInfo[result.size()]); + } + + private static MediaCodec createCodec(String encoderName) throws IOException { + if (encoderName != null) { + Ln.d("Creating encoder by name: '" + encoderName + "'"); + try { + return MediaCodec.createByCodecName(encoderName); + } catch (IllegalArgumentException e) { + MediaCodecInfo[] encoders = listEncoders(); + throw new InvalidEncoderException(encoderName, encoders); + } + } + MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); + Ln.d("Using encoder: '" + codec.getName() + "'"); + return codec; } private static void setCodecOption(MediaFormat format, CodecOption codecOption) { @@ -198,7 +222,11 @@ public class ScreenEncoder implements Device.RotationListener { } private static IBinder createDisplay() { - return SurfaceControl.createDisplay("scrcpy", true); + // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. + // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". + boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S" + .equals(Build.VERSION.CODENAME)); + return SurfaceControl.createDisplay("scrcpy", secure); } private static void configure(MediaCodec codec, MediaFormat format) { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index 10acfb5..c27322e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -82,6 +82,12 @@ public final class ScreenInfo { public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) { int rotation = displayInfo.getRotation(); + + if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { + // The user requested to lock the video orientation to the current orientation + lockedVideoOrientation = rotation; + } + Size deviceSize = displayInfo.getSize(); Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); if (crop != null) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 44b3afd..fc31dad 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,9 +1,8 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.ContentProvider; - import android.graphics.Rect; import android.media.MediaCodec; +import android.media.MediaCodecInfo; import android.os.BatteryManager; import android.os.Build; @@ -18,24 +17,25 @@ public final class 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()); - + private static void initAndCleanUp(Options options) { 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"); + Settings settings = Device.getSettings(); + if (options.getShowTouches()) { + try { + String oldValue = settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); // If "show touches" was disabled, it must be disabled back on clean up mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); + } catch (SettingsException e) { + Ln.e("Could not change \"show_touches\"", e); } + } - 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)); + if (options.getStayAwake()) { + int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; + try { + String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); try { restoreStayOn = Integer.parseInt(oldValue); if (restoreStayOn == stayOn) { @@ -45,23 +45,40 @@ public final class Server { } catch (NumberFormatException e) { restoreStayOn = 0; } + } catch (SettingsException e) { + Ln.e("Could not change \"stay_on_while_plugged_in\"", e); } } } - CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn); + try { + CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose()); + } catch (IOException e) { + Ln.e("Could not configure cleanup", e); + } + } + + 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 = options.getCodecOptions(); + + Thread initThread = startInitThread(options); boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { - ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions); + ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, + options.getEncoderName()); + Thread controllerThread = null; + Thread deviceMessageSenderThread = null; if (options.getControl()) { - final Controller controller = new Controller(device, connection); + final Controller controller = new Controller(device, connection, options.getClipboardAutosync()); // asynchronous - startController(controller); - startDeviceMessageSender(controller.getSender()); + controllerThread = startController(controller); + deviceMessageSenderThread = startDeviceMessageSender(controller.getSender()); device.setClipboardListener(new Device.ClipboardListener() { @Override @@ -77,12 +94,31 @@ public final class Server { } catch (IOException e) { // this is expected on close Ln.d("Screen streaming stopped"); + } finally { + initThread.interrupt(); + if (controllerThread != null) { + controllerThread.interrupt(); + } + if (deviceMessageSenderThread != null) { + deviceMessageSenderThread.interrupt(); + } } } } - private static void startController(final Controller controller) { - new Thread(new Runnable() { + private static Thread startInitThread(final Options options) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + initAndCleanUp(options); + } + }); + thread.start(); + return thread; + } + + private static Thread startController(final Controller controller) { + Thread thread = new Thread(new Runnable() { @Override public void run() { try { @@ -92,11 +128,13 @@ public final class Server { Ln.d("Controller stopped"); } } - }).start(); + }); + thread.start(); + return thread; } - private static void startDeviceMessageSender(final DeviceMessageSender sender) { - new Thread(new Runnable() { + private static Thread startDeviceMessageSender(final DeviceMessageSender sender) { + Thread thread = new Thread(new Runnable() { @Override public void run() { try { @@ -106,7 +144,9 @@ public final class Server { Ln.d("Device message sender stopped"); } } - }).start(); + }); + thread.start(); + return thread; } private static Options createOptions(String... args) { @@ -120,58 +160,93 @@ public final class Server { "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - final int expectedParameters = 14; - if (args.length != expectedParameters) { - throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); - } - Options options = new Options(); - 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[3]); - options.setBitRate(bitRate); - - 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[6]); - options.setTunnelForward(tunnelForward); - - Rect crop = parseCrop(args[7]); - options.setCrop(crop); - - boolean sendFrameMeta = Boolean.parseBoolean(args[8]); - options.setSendFrameMeta(sendFrameMeta); - - 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); + for (int i = 1; i < args.length; ++i) { + String arg = args[i]; + int equalIndex = arg.indexOf('='); + if (equalIndex == -1) { + throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\""); + } + String key = arg.substring(0, equalIndex); + String value = arg.substring(equalIndex + 1); + switch (key) { + case "log_level": + Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); + options.setLogLevel(level); + break; + case "max_size": + int maxSize = Integer.parseInt(value) & ~7; // multiple of 8 + options.setMaxSize(maxSize); + break; + case "bit_rate": + int bitRate = Integer.parseInt(value); + options.setBitRate(bitRate); + break; + case "max_fps": + int maxFps = Integer.parseInt(value); + options.setMaxFps(maxFps); + break; + case "lock_video_orientation": + int lockVideoOrientation = Integer.parseInt(value); + options.setLockVideoOrientation(lockVideoOrientation); + break; + case "tunnel_forward": + boolean tunnelForward = Boolean.parseBoolean(value); + options.setTunnelForward(tunnelForward); + break; + case "crop": + Rect crop = parseCrop(value); + options.setCrop(crop); + break; + case "send_frame_meta": + boolean sendFrameMeta = Boolean.parseBoolean(value); + options.setSendFrameMeta(sendFrameMeta); + break; + case "control": + boolean control = Boolean.parseBoolean(value); + options.setControl(control); + break; + case "display_id": + int displayId = Integer.parseInt(value); + options.setDisplayId(displayId); + break; + case "show_touches": + boolean showTouches = Boolean.parseBoolean(value); + options.setShowTouches(showTouches); + break; + case "stay_awake": + boolean stayAwake = Boolean.parseBoolean(value); + options.setStayAwake(stayAwake); + break; + case "codec_options": + List codecOptions = CodecOption.parse(value); + options.setCodecOptions(codecOptions); + break; + case "encoder_name": + if (!value.isEmpty()) { + options.setEncoderName(value); + } + break; + case "power_off_on_close": + boolean powerOffScreenOnClose = Boolean.parseBoolean(value); + options.setPowerOffScreenOnClose(powerOffScreenOnClose); + break; + case "clipboard_autosync": + boolean clipboardAutosync = Boolean.parseBoolean(value); + options.setClipboardAutosync(clipboardAutosync); + break; + default: + Ln.w("Unknown server option: " + key); + break; + } + } return options; } private static Rect parseCrop(String crop) { - if ("-".equals(crop)) { + if (crop.isEmpty()) { return null; } // input format: "width:height:x:y" @@ -206,6 +281,15 @@ public final class Server { Ln.e(" scrcpy --display " + id); } } + } else if (e instanceof InvalidEncoderException) { + InvalidEncoderException iee = (InvalidEncoderException) e; + MediaCodecInfo[] encoders = iee.getAvailableEncoders(); + if (encoders != null && encoders.length > 0) { + Ln.e("Try to use one of the available encoders:"); + for (MediaCodecInfo encoder : encoders) { + Ln.e(" scrcpy --encoder '" + encoder.getName() + "'"); + } + } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/Settings.java new file mode 100644 index 0000000..cb15ebb --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Settings.java @@ -0,0 +1,84 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.ContentProvider; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.os.Build; + +import java.io.IOException; + +public class Settings { + + public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM; + public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE; + public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL; + + private final ServiceManager serviceManager; + + public Settings(ServiceManager serviceManager) { + this.serviceManager = serviceManager; + } + + private static void execSettingsPut(String table, String key, String value) throws SettingsException { + try { + Command.exec("settings", "put", table, key, value); + } catch (IOException | InterruptedException e) { + throw new SettingsException("put", table, key, value, e); + } + } + + private static String execSettingsGet(String table, String key) throws SettingsException { + try { + return Command.execReadLine("settings", "get", table, key); + } catch (IOException | InterruptedException e) { + throw new SettingsException("get", table, key, null, e); + } + } + + public String getValue(String table, String key) throws SettingsException { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { + return provider.getValue(table, key); + } catch (SettingsException e) { + Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e); + } + } + + return execSettingsGet(table, key); + } + + public void putValue(String table, String key, String value) throws SettingsException { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { + provider.putValue(table, key, value); + } catch (SettingsException e) { + Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e); + } + } + + execSettingsPut(table, key, value); + } + + public String getAndPutValue(String table, String key, String value) throws SettingsException { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { + String oldValue = provider.getValue(table, key); + if (!value.equals(oldValue)) { + provider.putValue(table, key, value); + } + return oldValue; + } catch (SettingsException e) { + Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e); + } + } + + 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/SettingsException.java b/server/src/main/java/com/genymobile/scrcpy/SettingsException.java new file mode 100644 index 0000000..36ef63e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/SettingsException.java @@ -0,0 +1,11 @@ +package com.genymobile.scrcpy; + +public class SettingsException extends Exception { + private static String createMessage(String method, String table, String key, String value) { + return "Could not access settings: " + method + " " + table + " " + key + (value != null ? " " + value : ""); + } + + public SettingsException(String method, String table, String key, String value, Throwable cause) { + super(createMessage(method, table, key, value), cause); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 351cc57..0f473bc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -16,6 +16,7 @@ public final class Workarounds { // not instantiable } + @SuppressWarnings("deprecation") public static void prepareMainLooper() { // Some devices internally create a Handler when creating an input Surface, causing an exception: // "Can't create handler inside thread that has not called Looper.prepare()" diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index 71967c5..93ed452 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -14,7 +14,7 @@ public class ActivityManager { private final IInterface manager; private Method getContentProviderExternalMethod; - private boolean getContentProviderExternalMethodLegacy; + private boolean getContentProviderExternalMethodNewVersion = true; private Method removeContentProviderExternalMethod; public ActivityManager(IInterface manager) { @@ -29,7 +29,7 @@ public class ActivityManager { } catch (NoSuchMethodException e) { // old version getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class); - getContentProviderExternalMethodLegacy = true; + getContentProviderExternalMethodNewVersion = false; } } return getContentProviderExternalMethod; @@ -46,7 +46,7 @@ public class ActivityManager { try { Method method = getGetContentProviderExternalMethod(); Object[] args; - if (!getContentProviderExternalMethodLegacy) { + if (getContentProviderExternalMethodNewVersion) { // new version args = new Object[]{name, ServiceManager.USER_ID, token, null}; } else { diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index b43494c..47eae64 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -1,7 +1,9 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.SettingsException; +import android.annotation.SuppressLint; import android.os.Bundle; import android.os.IBinder; @@ -35,7 +37,9 @@ public class ContentProvider implements Closeable { private final IBinder token; private Method callMethod; - private boolean callMethodLegacy; + private int callMethodVersion; + + private Object attributionSource; ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) { this.manager = manager; @@ -44,32 +48,69 @@ public class ContentProvider implements Closeable { this.token = token; } + @SuppressLint("PrivateApi") 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; + Class attributionSourceClass = Class.forName("android.content.AttributionSource"); + callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class); + callMethodVersion = 0; + } catch (NoSuchMethodException | ClassNotFoundException e0) { + // old versions + try { + callMethod = provider.getClass() + .getMethod("call", String.class, String.class, String.class, String.class, String.class, Bundle.class); + callMethodVersion = 1; + } catch (NoSuchMethodException e1) { + try { + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class); + callMethodVersion = 2; + } catch (NoSuchMethodException e2) { + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class); + callMethodVersion = 3; + } + } } } return callMethod; } - private Bundle call(String callMethod, String arg, Bundle extras) { + @SuppressLint("PrivateApi") + private Object getAttributionSource() + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + if (attributionSource == null) { + Class cl = Class.forName("android.content.AttributionSource$Builder"); + Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID); + cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME); + attributionSource = cl.getDeclaredMethod("build").invoke(builder); + } + + return attributionSource; + } + + private Bundle call(String callMethod, String arg, Bundle extras) + throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { 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}; + switch (callMethodVersion) { + case 0: + args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras}; + break; + case 1: + args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; + break; + case 2: + args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras}; + break; + default: + args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras}; + break; } return (Bundle) method.invoke(provider, args); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) { Ln.e("Could not invoke method", e); - return null; + throw e; } } @@ -103,30 +144,31 @@ public class ContentProvider implements Closeable { } } - public String getValue(String table, String key) { + public String getValue(String table, String key) throws SettingsException { 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; + try { + Bundle bundle = call(method, key, arg); + if (bundle == null) { + return null; + } + return bundle.getString("value"); + } catch (Exception e) { + throw new SettingsException(table, "get", key, null, e); } - return bundle.getString("value"); + } - public void putValue(String table, String key, String value) { + public void putValue(String table, String key, String value) throws SettingsException { 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); + try { + call(method, key, arg); + } catch (Exception e) { + throw new SettingsException(table, "put", key, value, e); } - return oldValue; } } 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 c4ce59c..6f4b9c0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -77,7 +77,14 @@ public final class ServiceManager { public ClipboardManager getClipboardManager() { if (clipboardManager == null) { - clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard")); + IInterface clipboard = getService("clipboard", "android.content.IClipboard"); + if (clipboard == null) { + // Some devices have no clipboard manager + // + // + return null; + } + clipboardManager = new ClipboardManager(clipboard); } return clipboardManager; } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 6f8941b..7a19e6e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -11,6 +11,9 @@ public class StatusBarManager { private final IInterface manager; private Method expandNotificationsPanelMethod; + private boolean expandNotificationPanelMethodCustomVersion; + private Method expandSettingsPanelMethod; + private boolean expandSettingsPanelMethodNewVersion = true; private Method collapsePanelsMethod; public StatusBarManager(IInterface manager) { @@ -19,11 +22,31 @@ public class StatusBarManager { private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException { if (expandNotificationsPanelMethod == null) { - expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); + try { + expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); + } catch (NoSuchMethodException e) { + // Custom version for custom vendor ROM: + expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel", int.class); + expandNotificationPanelMethodCustomVersion = true; + } } return expandNotificationsPanelMethod; } + private Method getExpandSettingsPanel() throws NoSuchMethodException { + if (expandSettingsPanelMethod == null) { + try { + // Since Android 7: https://android.googlesource.com/platform/frameworks/base.git/+/a9927325eda025504d59bb6594fee8e240d95b01%5E%21/ + expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel", String.class); + } catch (NoSuchMethodException e) { + // old version + expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel"); + expandSettingsPanelMethodNewVersion = false; + } + } + return expandSettingsPanelMethod; + } + private Method getCollapsePanelsMethod() throws NoSuchMethodException { if (collapsePanelsMethod == null) { collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); @@ -34,7 +57,26 @@ public class StatusBarManager { public void expandNotificationsPanel() { try { Method method = getExpandNotificationsPanelMethod(); - method.invoke(manager); + if (expandNotificationPanelMethodCustomVersion) { + method.invoke(manager, 0); + } else { + method.invoke(manager); + } + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + } + } + + public void expandSettingsPanel() { + try { + Method method = getExpandSettingsPanel(); + if (expandSettingsPanelMethodNewVersion) { + // new version + method.invoke(manager, (Object) null); + } else { + // old version + method.invoke(manager); + } } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index f5fa4d0..5e79d4f 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -2,7 +2,6 @@ package com.genymobile.scrcpy; import android.view.KeyEvent; import android.view.MotionEvent; - import org.junit.Assert; import org.junit.Test; @@ -25,6 +24,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(5); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); @@ -37,6 +37,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(5, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); } @@ -48,7 +49,7 @@ public class ControlMessageReaderTest { DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); byte[] text = "testé".getBytes(StandardCharsets.UTF_8); - dos.writeShort(text.length); + dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); @@ -68,7 +69,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH]; Arrays.fill(text, (byte) 'a'); - dos.writeShort(text.length); + dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); @@ -152,6 +153,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON); + dos.writeByte(KeyEvent.ACTION_UP); byte[] packet = bos.toByteArray(); @@ -159,6 +161,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType()); + Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); } @Test @@ -178,19 +181,35 @@ public class ControlMessageReaderTest { } @Test - public void testParseCollapseNotificationPanelEvent() throws IOException { + public void testParseExpandSettingsPanelEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL); + dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); - Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType()); + } + + @Test + public void testParseCollapsePanelsEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType()); } @Test @@ -200,6 +219,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD); + dos.writeByte(ControlMessage.COPY_KEY_COPY); byte[] packet = bos.toByteArray(); @@ -207,6 +227,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); + Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey()); } @Test @@ -216,9 +237,10 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); + dos.writeLong(0x0102030405060708L); // sequence dos.writeByte(1); // paste byte[] text = "testé".getBytes(StandardCharsets.UTF_8); - dos.writeShort(text.length); + dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); @@ -227,10 +249,9 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); + Assert.assertEquals(0x0102030405060708L, event.getSequence()); Assert.assertEquals("testé", event.getText()); - - boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0; - Assert.assertTrue(parse); + Assert.assertTrue(event.getPaste()); } @Test @@ -242,11 +263,12 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH]; + dos.writeLong(0x0807060504030201L); // sequence dos.writeByte(1); // paste Arrays.fill(rawText, (byte) 'a'); String text = new String(rawText, 0, rawText.length); - dos.writeShort(rawText.length); + dos.writeInt(rawText.length); dos.write(rawText); byte[] packet = bos.toByteArray(); @@ -255,10 +277,9 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); + Assert.assertEquals(0x0807060504030201L, event.getSequence()); Assert.assertEquals(text, event.getText()); - - boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0; - Assert.assertTrue(parse); + Assert.assertTrue(event.getPaste()); } @Test @@ -308,11 +329,13 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(0); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(MotionEvent.ACTION_DOWN); dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(1); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); @@ -322,12 +345,14 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(0, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(1, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); } @@ -341,6 +366,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(4); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); @@ -353,6 +379,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(4, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); event = reader.next(); @@ -360,6 +387,7 @@ public class ControlMessageReaderTest { bos.reset(); dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(5); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); @@ -369,6 +397,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(5, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); } } diff --git a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java index df12f64..7b917d3 100644 --- a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java @@ -19,7 +19,7 @@ public class DeviceMessageWriterTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); - dos.writeShort(data.length); + dos.writeInt(data.length); dos.write(data); byte[] expected = bos.toByteArray(); @@ -32,4 +32,24 @@ public class DeviceMessageWriterTest { Assert.assertArrayEquals(expected, actual); } + + @Test + public void testSerializeAckSetClipboard() throws IOException { + DeviceMessageWriter writer = new DeviceMessageWriter(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(DeviceMessage.TYPE_ACK_CLIPBOARD); + dos.writeLong(0x0102030405060708L); + + byte[] expected = bos.toByteArray(); + + DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L); + bos = new ByteArrayOutputStream(); + writer.writeTo(msg, bos); + + byte[] actual = bos.toByteArray(); + + Assert.assertArrayEquals(expected, actual); + } } From c71942cda67a01ba91e596c79196007ded2dca30 Mon Sep 17 00:00:00 2001 From: Barry <870709864@qq.com> Date: Sun, 9 Jan 2022 16:12:11 +0800 Subject: [PATCH 6/6] feat: copy vcruntime dll --- ci/win/publish_for_win.bat | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ci/win/publish_for_win.bat b/ci/win/publish_for_win.bat index b9dfcdd..66fc565 100644 --- a/ci/win/publish_for_win.bat +++ b/ci/win/publish_for_win.bat @@ -100,10 +100,17 @@ if /i %cpu_mode% == x86 ( ) :: copy vcruntime dll -:: 只有在64位下需要这个 if /i %cpu_mode% == x64 ( - cp "C:\Windows\System32\vcruntime140_1.dll" %publish_path%\vcruntime140_1.dll cp "C:\Windows\System32\msvcp140_1.dll" %publish_path%\msvcp140_1.dll + cp "C:\Windows\System32\msvcp140.dll" %publish_path%\msvcp140.dll + cp "C:\Windows\System32\vcruntime140.dll" %publish_path%\vcruntime140.dll + :: 只有x64需要 + cp "C:\Windows\System32\vcruntime140_1.dll" %publish_path%\vcruntime140_1.dll +) else ( + cp "C:\Windows\SysWOW64\msvcp140_1.dll" %publish_path%\msvcp140_1.dll + cp "C:\Windows\SysWOW64\msvcp140.dll" %publish_path%\msvcp140.dll + cp "C:\Windows\SysWOW64\vcruntime140.dll" %publish_path%\vcruntime140.dll + ) ::cp "C:\Program Files (x86)\Microsoft Visual Studio\Installer\VCRUNTIME140.dll" %publish_path%\VCRUNTIME140.dll