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 260604ee5..a19be58c9 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 ad1483527..c0bc62695 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 a19be58c9..d90862c28 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 adef61298..07281a014 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 8cfebadf7..009a65c23 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 6968d5c21..d99bafe9f 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 105ba56af..ba7a815ae 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 7c833a90f..98901d627 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 5616622ed..756da4b35 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 496398b63..179357671 100644 --- a/QtScrcpy/device/server/server.h +++ b/QtScrcpy/device/server/server.h @@ -31,7 +31,7 @@ class Server : public QObject 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 60f619191..6c98b075d 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 6b85845f6..7f6193496 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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 I&HD%YAL6Gm5dZ)H literal 34930 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 C4`KxX 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 | 667 +++++++++++------- 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, 515 insertions(+), 801 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 ba7a815ae..1fc8b8df6 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 89ec6f829..5eca0d155 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 bfddbb2e7..027ae56fd 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 93a2a8c66..5a6f350f4 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 4666405c3..7762e2aba 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 049cf629c..fac62696a 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" - ) + # disable some warning + add_compile_options(-Wno-nested-anon-types -Wno-c++17-extensions) +endif() + +# +# Qt +# - # QS_MAC_CONFIG: Config file stored in Contents/MacOS/config - set(QS_MAC_CONFIG "${PROJECT_SOURCE_DIR}/config/config.ini") +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() -set(QS_TS_FILES - ${CMAKE_CURRENT_SOURCE_DIR}/res/i18n/zh_CN.ts - ${CMAKE_CURRENT_SOURCE_DIR}/res/i18n/en_US.ts +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 ) -set_source_files_properties(${QS_TS_FILES} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/res/i18n") +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}) -set(QS_SOURCES_MAIN +# 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() +# plantform file +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + # Define VERSION macros for .rc file + add_compile_definitions( + 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" + ) +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() +# 使用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 +) +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}) + +# 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} +) +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() -# ******************** Microsoft Windows ******************** -if(WIN32) - message(STATUS "[QtScrcpy] Make for Microsoft Windows.") - - # 通过rc的方式的话,VERSION变量rc中获取不到,定义为宏方便rc中使用 - # Define 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}\\\" - ) - 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() +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 +) - # 构建完成后复制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() +# 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:>" +) - 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() +# +# 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() -target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +# 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 + ) -set(QS_SUBDIRECTORIES_MAIN - adb - common - device - devicemanage - fontawesome - uibase - util -) -foreach(QS_SUBDIRECTORY_MAIN ${QS_SUBDIRECTORIES_MAIN}) - add_subdirectory(${QS_SUBDIRECTORY_MAIN}) -endforeach() + # 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" + ) -target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC - adb - devicemanage + # 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 ) -target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE - Qt${QT_VERSION_MAJOR}::Widgets - Qt${QT_VERSION_MAJOR}::Network - device - stream - ui - util + # 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 "" ) -if(QT_VERSION_MAJOR EQUAL 6) - qt_finalize_executable(${CMAKE_PROJECT_NAME}) + # 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 990537ed2..720f01838 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 3644ade95..000000000 --- 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 cd2b0c652..000000000 --- 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 6e8b83711..000000000 --- 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 432d74916..000000000 --- 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 cf3425431..000000000 --- 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 2c5cd44a3..000000000 --- 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 a74b05244..000000000 --- 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 ee6ced2e3..000000000 --- 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 b2931a28f..000000000 --- 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 17de9e265..000000000 --- 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 12144dbd9..000000000 --- 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 ee3a1405e..000000000 --- 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 eb7fe84b7..000000000 --- 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 756da4b35..1343b331a 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 469d2d3a5..000000000 --- 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 f8f632505..000000000 --- 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 8ce0ab7e1..000000000 --- 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 ead0947e2..000000000 --- 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 929580548..31b501d0e 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 3e25f20ae..007a4b715 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 861883a54..000000000 --- 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 2d92d13d6..000000000 --- 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 846851bfc..000000000 --- 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 dc1e644a1..77d6f4ca2 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 b67c72e44..5e21bb17b 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 aa5990fd3..a3e6dd0c8 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 -fi -mkdir $temp_path -cd $temp_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" +# 删除编译目录 +build_path=$script_path/../build_temp +if [ -d "$build_path" ]; then + rm -rf $build_path fi +mkdir $build_path +cd $build_path -# 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 deb4ef145..c16b42600 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 edf2258f5..0e7e584ea 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 "%1"=="Release" ( goto build_mode_ok ) -if /i "%1"=="release" ( - set build_mode=release +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 5b561122d..b9dfcdd17 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 7f6193496..3a73dcd51 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 000000000..0df7064d6 --- /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 c8ff85d64..1f939a1ae 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 000000000..0f86c29f0 --- /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 000000000..984daf3b2 --- /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 74555636b..319a957d1 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 static void main(String... args) { 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 1897bda3a..12f2a8899 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 Object getValue() { } 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 000000000..0ef976a66 --- /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 7d0ab7a65..63ba0fa36 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 static ControlMessage createInjectScrollEvent(Position position, int hScr 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 int getVScroll() { 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 fbf49a61e..f09ed26f0 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 @@ 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) - public static final int INJECT_TEXT_MAX_LENGTH = 300; + private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k - private static final int RAW_BUFFER_SIZE = 4096; + 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 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 ControlMessage next() { 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 @@ private ControlMessage parseInjectKeycode() { } 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 @@ private ControlMessage parseInjectScrollEvent() { 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 960c6a6e5..9246004a1 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.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 @@ private void initPointers() { 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 @@ private void initPointers() { 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 @@ private void handleEvent() throws IOException { 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 @@ private void handleEvent() throws IOException { 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_EXPAND_SETTINGS_PANEL: + Device.expandSettingsPanel(); break; - case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: - device.collapsePanels(); + 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 @@ private boolean injectChar(char c) { return false; } for (KeyEvent event : events) { - if (!device.injectEvent(event)) { + if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -166,7 +178,7 @@ private boolean injectTouch(int action, long pointerId, Position position, float 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 @@ private boolean injectTouch(int action, long pointerId, Position position, float } } + // 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 @@ private boolean injectScroll(Position position, int hScroll, int vScroll) { 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); + } + + /** + * 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 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 boolean pressBackOrTurnScreenOn() { - int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; - return device.injectKeycode(keycode); + 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) { + 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 @@ private boolean setClipboard(String text, boolean paste) { // 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 349486c35..ba833a06c 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 interface ClipboardListener { void onClipboardTextChanged(String text); } - private final ServiceManager serviceManager = new ServiceManager(); - private ScreenInfo screenInfo; private RotationListener rotationListener; private ClipboardListener clipboardListener; @@ -53,18 +61,18 @@ public interface ClipboardListener { 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 void onRotationChanged(int rotation) { } }, 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 static String getDeviceName() { 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 boolean injectEvent(InputEvent inputEvent, int mode) { 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 injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { + return injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); + } + + 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 injectKeycode(int keyCode) { - return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); + public boolean pressReleaseKeycode(int keyCode, int injectMode) { + return pressReleaseKeycode(keyCode, displayId, injectMode); } - public boolean isScreenOn() { - return serviceManager.getPowerManager().isScreenOn(); + public static boolean isScreenOn() { + return SERVICE_MANAGER.getPowerManager().isScreenOn(); } public synchronized void setRotationListener(RotationListener rotationListener) { @@ -190,16 +216,24 @@ public synchronized void setClipboardListener(ClipboardListener clipboardListene 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 String getClipboardText() { } 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 boolean setScreenPowerMode(int mode) { 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 void rotateDevice() { } } - 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 c6eebd380..5b7c4de5b 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 @@ 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 static DeviceMessage createClipboard(String text) { 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 int getType() { 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 bbf4dd2ee..4ebccaccf 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 synchronized void pushClipboardText(String text) { 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 6c7f36343..bcd8d2067 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 @@ 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 000000000..1efd2989d --- /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 c218fa0fc..c39fc621c 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 static boolean isEnabled(Level level) { 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 static void i(String message) { } } - 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 06312a37a..1ac171766 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 @@ 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 void setMaxFps(int maxFps) { 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 void setStayAwake(boolean stayAwake) { 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 d722388c8..f98c53d00 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 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 boolean consumeRotationChange() { 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 @@ private void internalStreamScreen(Device device, FileDescriptor fd) throws IOExc 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 @@ private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, 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 @@ private static MediaFormat createFormat(int bitRate, int maxFps, 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 @@ private static void scrcpy(Options options) throws IOException { } 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 void onClipboardTextChanged(String text) { } 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 void run() { 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 void run() { Ln.d("Device message sender stopped"); } } - }).start(); + }); + thread.start(); + return thread; } private static Options createOptions(String... args) { @@ -120,58 +160,93 @@ private static Options createOptions(String... args) { "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 @@ private static void suggestFix(Throwable e) { 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 000000000..cb15ebb46 --- /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 000000000..36ef63ee1 --- /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 351cc574c..0f473bc1b 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 @@ private 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 71967c500..93ed45287 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 @@ private Method getGetContentProviderExternalMethod() throws NoSuchMethodExceptio } 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 @@ private ContentProvider getContentProviderExternal(String name, IBinder token) { 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 b43494c77..47eae64d1 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 @@ private static String getPutMethod(String table) { } } - 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 c4ce59c2e..6f4b9c042 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 StatusBarManager getStatusBarManager() { 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 6f8941bdf..7a19e6e5a 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 StatusBarManager(IInterface manager) { 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 @@ private Method getCollapsePanelsMethod() throws NoSuchMethodException { 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 f5fa4d09f..5e79d4f00 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 @@ import android.view.KeyEvent; import android.view.MotionEvent; - import org.junit.Assert; import org.junit.Test; @@ -25,6 +24,7 @@ public void testParseKeycodeEvent() throws IOException { 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 void testParseKeycodeEvent() throws IOException { 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 void testParseTextEvent() throws IOException { 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 void testParseLongTextEvent() throws IOException { 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 void testParseBackOrScreenOnEvent() throws IOException { 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 void testParseBackOrScreenOnEvent() throws IOException { 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 void testParseExpandNotificationPanelEvent() throws IOException { } @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_EXPAND_SETTINGS_PANEL); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + 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_NOTIFICATION_PANEL); + dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS); 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_COLLAPSE_PANELS, event.getType()); } @Test @@ -200,6 +219,7 @@ public void testParseGetClipboardEvent() throws IOException { 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 void testParseGetClipboardEvent() throws IOException { 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 void testParseSetClipboardEvent() throws IOException { 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 void testParseSetClipboardEvent() throws IOException { 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 void testParseBigSetClipboardEvent() throws IOException { 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 void testParseBigSetClipboardEvent() throws IOException { 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 void testMultiEvents() throws IOException { 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 void testMultiEvents() throws IOException { 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 void testPartialEvents() throws IOException { 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 void testPartialEvents() throws IOException { 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 void testPartialEvents() throws IOException { 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 void testPartialEvents() throws IOException { 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 df12f647b..7b917d337 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 void testSerializeClipboard() throws IOException { 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 void testSerializeClipboard() throws IOException { 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 b9dfcdd17..66fc56563 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