From 3d0ada63a06951ef9c23f6975430c4e31506cd3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Han=C3=A7erli?= <burak.hancerli@qt.io> Date: Tue, 14 May 2024 13:00:25 +0000 Subject: [PATCH] QDS-12320 Reimplement device as client --- .gitignore | 4 + .gitmodules | 14 +- CMakeLists.txt | 4 +- cicd/gitlab-ci.yml | 5 + cicd/stages/build.yml | 4 +- cicd/stages/test.yml | 12 +- src/CMakeLists.txt | 25 +- ...oidManifest.xml => AndroidManifest.xml.in} | 3 +- src/backend/backend.cpp | 243 ++++++++------ src/backend/backend.h | 27 +- src/backend/constants.h | 54 ++++ src/backend/dsconnector.cpp | 128 -------- src/backend/dsconnector/ds.cpp | 236 ++++++++++++++ src/backend/dsconnector/ds.h | 99 ++++++ src/backend/dsconnector/dsdiscovery.cpp | 86 +++++ .../dsdiscovery.h} | 45 +-- src/backend/dsconnector/dsmanager.cpp | 187 +++++++++++ src/backend/dsconnector/dsmanager.h | 69 ++++ src/backend/dsconnector/tcpdatatypes.h | 45 +++ src/backend/projectmanager.cpp | 47 +-- src/backend/projectmanager.h | 4 +- src/ui/DSManagement.qml | 35 ++ src/ui/HomePage.qml | 45 --- src/ui/Network.qml | 55 ---- src/ui/SettingsPage.qml | 15 - src/ui/content/images/ds_icon.png | Bin 0 -> 10212 bytes src/ui/main.qml | 125 +++++++- tests/CMakeLists.txt | 13 +- tests/main.cpp | 103 ++++++ tests/mock.h | 99 ++++++ tests/scripts/get_test_result.sh | 28 ++ tests/tst_designstudio.cpp | 301 ++++++++++++++++++ tests/tst_designstudio.h | 62 ++++ tests/tst_dsdiscovery.cpp | 67 ++++ tests/tst_dsdiscovery.h | 41 +++ tests/tst_dsmanager.cpp | 59 ++++ tests/tst_dsmanager.h | 44 +++ ..._qtuiviewer.cpp => tst_projectmanager.cpp} | 161 ++-------- ...{tst_qtuiviewer.h => tst_projectmanager.h} | 19 +- tests/tst_serviceconnector.cpp | 68 ++++ tests/tst_serviceconnector.h | 43 +++ 41 files changed, 2108 insertions(+), 616 deletions(-) rename src/android/{AndroidManifest.xml => AndroidManifest.xml.in} (94%) create mode 100644 src/backend/constants.h delete mode 100644 src/backend/dsconnector.cpp create mode 100644 src/backend/dsconnector/ds.cpp create mode 100644 src/backend/dsconnector/ds.h create mode 100644 src/backend/dsconnector/dsdiscovery.cpp rename src/backend/{dsconnector.h => dsconnector/dsdiscovery.h} (58%) create mode 100644 src/backend/dsconnector/dsmanager.cpp create mode 100644 src/backend/dsconnector/dsmanager.h create mode 100644 src/backend/dsconnector/tcpdatatypes.h create mode 100644 src/ui/DSManagement.qml delete mode 100644 src/ui/Network.qml create mode 100644 src/ui/content/images/ds_icon.png create mode 100644 tests/main.cpp create mode 100644 tests/mock.h create mode 100644 tests/scripts/get_test_result.sh create mode 100644 tests/tst_designstudio.cpp create mode 100644 tests/tst_designstudio.h create mode 100644 tests/tst_dsdiscovery.cpp create mode 100644 tests/tst_dsdiscovery.h create mode 100644 tests/tst_dsmanager.cpp create mode 100644 tests/tst_dsmanager.h rename tests/{tst_qtuiviewer.cpp => tst_projectmanager.cpp} (53%) rename tests/{tst_qtuiviewer.h => tst_projectmanager.h} (82%) create mode 100644 tests/tst_serviceconnector.cpp create mode 100644 tests/tst_serviceconnector.h diff --git a/.gitignore b/.gitignore index a8f3c8c..8d36dec 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ src/ui/*.db-wal src/ui/share.qrc logcat.txt output.junit.xml +src/ui/DesignViewer.qmlproject.qtds +src/android/AndroidManifest.xml +logcat.txt +output.junit.xml diff --git a/.gitmodules b/.gitmodules index e3877e6..bda2b24 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,6 @@ -[submodule "qtquickdesigner-components"] -path = 3rdparty/qtquickdesigner-components -url = https://git.qt.io/design-studio/kit-dependencies/qt-quickdesigner-components.git [submodule "3rdparty/zxing-cpp"] - path = 3rdparty/zxing-cpp - url = https://github.com/zxing-cpp/zxing-cpp.git +path = 3rdparty/zxing-cpp +url = https://github.com/zxing-cpp/zxing-cpp.git [submodule "3rdparty/qtquickdesigner-components"] - path = 3rdparty/qtquickdesigner-components - url = https://git.qt.io/design-studio/kit-dependencies/qt-quickdesigner-components -[submodule "3rdparty/googletest"] - path = 3rdparty/googletest - url = https://github.com/google/googletest.git +path = 3rdparty/qtquickdesigner-components +url = https://codereview.qt-project.org/qt-labs/qtquickdesigner-components diff --git a/CMakeLists.txt b/CMakeLists.txt index dd4c369..d21fb8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ find_package( find_package(Qt6 REQUIRED COMPONENTS Core) qt_policy(SET QTP0002 NEW) -set(QT_MINIMUM_VERSION 6.6.1) +set(QT_MINIMUM_VERSION 6.7.0) if(QT_VERSION VERSION_LESS QT_MINIMUM_VERSION) message(FATAL_ERROR "Minimum supported Qt version: ${QT_MINIMUM_VERSION}") endif() @@ -40,8 +40,8 @@ add_definitions( -DCMAKE_VAR_GIT_VERSION="${CMAKE_VAR_GIT_VERSION}" ) add_definitions( -DCMAKE_VAR_QT_QUICK_COMPONENTS_VERSION="${CMAKE_VAR_QT_QUICK_COMPONENTS_VERSION}" ) add_definitions( -DCMAKE_VAR_ZXING_VERSION="${CMAKE_VAR_ZXING_VERSION}" ) -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/zxing-cpp) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/tests) message(STATUS "PROJECT VERSION: ${CMAKE_VAR_GIT_VERSION}") diff --git a/cicd/gitlab-ci.yml b/cicd/gitlab-ci.yml index 8dd8ae4..0b3765d 100644 --- a/cicd/gitlab-ci.yml +++ b/cicd/gitlab-ci.yml @@ -6,6 +6,11 @@ variables: workflow: name: 'Qt-${QDS_CI_QT_VERSION} - ${CI_COMMIT_MESSAGE}' + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + - if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS' + when: never + stages: - build diff --git a/cicd/stages/build.yml b/cicd/stages/build.yml index 4dd6505..b6dde85 100644 --- a/cicd/stages/build.yml +++ b/cicd/stages/build.yml @@ -3,8 +3,8 @@ build-android: extends: .pipeline_common stage: build - rules: - - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "web" + # rules: + # - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "web" parallel: matrix: - QDS_CI_JOB_TARGET_ARCH: "arm64_v8a" diff --git a/cicd/stages/test.yml b/cicd/stages/test.yml index 5c9943a..4912a8b 100644 --- a/cicd/stages/test.yml +++ b/cicd/stages/test.yml @@ -28,7 +28,7 @@ test-x86_64: echo "Waiting for test to start" sleep 0.1 counter=$((counter+1)) - if [ $counter -gt 60 ]; then + if [ $counter -gt 180 ]; then echo "Test did not start in time" exit 1 fi @@ -38,11 +38,11 @@ test-x86_64: - | counter=0 while [ -n "$(adb shell pidof -s io.qt.qtuiviewer.test)" ]; do - echo "Waiting for test to finish" - sleep 0.1 + echo "Waiting for test to finish with PID ${PID_OF_TEST}" + sleep 1 counter=$((counter+1)) - if [ $counter -gt 60 ]; then - echo "Test did not finish in time" + if [ $counter -gt 180 ]; then + echo "Test did not finish in time or did not start" exit 1 fi done @@ -50,6 +50,8 @@ test-x86_64: - adb shell "run-as io.qt.qtuiviewer.test cat /data/data/io.qt.qtuiviewer.test/files/output.junitxml" > output.junit.xml - adb uninstall io.qt.qtuiviewer.test - mv output.junit.xml ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml + - chmod +x ${CI_PROJECT_DIR}/tests/scripts/get_test_result.sh + - ${CI_PROJECT_DIR}/tests/scripts/get_test_result.sh ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml artifacts: paths: - ${QDS_CI_JOB_TEST_RESULTS_PATH}/ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d991bbb..5d4948f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,16 +1,21 @@ find_package( Qt6 - COMPONENTS Core Widgets Quick Gui Qml Multimedia MultimediaWidgets Concurrent + COMPONENTS Core Widgets Quick Gui Qml Multimedia MultimediaWidgets Concurrent Network WebSockets REQUIRED ) +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network WebSockets) + qt_add_executable(${PROJECT_NAME} backend/importdummy.qml backend/main.cpp backend/backend.cpp backend/backend.h backend/serviceconnector.cpp backend/serviceconnector.h backend/projectmanager.cpp backend/projectmanager.h - backend/dsconnector.cpp backend/dsconnector.h + backend/dsconnector/ds.cpp backend/dsconnector/ds.h + backend/dsconnector/dsdiscovery.cpp backend/dsconnector/dsdiscovery.h + backend/dsconnector/dsmanager.cpp backend/dsconnector/dsmanager.h + backend/dsconnector/tcpdatatypes.h backend/qrscanner.cpp backend/qrscanner.h ui/main.qml ../3rdparty/zxing-cpp/example/ZXingQtReader.h @@ -30,7 +35,7 @@ qt_add_resources(${PROJECT_NAME} "qml" ui/main.qml ui/HomePage.qml ui/Logs.qml - ui/Network.qml + ui/DSManagement.qml ui/ExamplesPage.qml ui/AboutHeader.qml ui/SettingsPage.qml @@ -41,7 +46,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Quick Qt6::Gui Qt6::Qml Qt6::GuiPrivate Qt6::Multimedia Qt6::MultimediaWidgets - Qt6::Concurrent + Qt6::Concurrent Qt6::Network Qt6::WebSockets ZXing::ZXing ) @@ -49,7 +54,10 @@ qt_add_library(qtuiviewerlib OBJECT EXCLUDE_FROM_ALL backend/projectmanager.cpp backend/projectmanager.h backend/serviceconnector.cpp backend/serviceconnector.h - backend/dsconnector.cpp backend/dsconnector.h + backend/dsconnector/ds.cpp backend/dsconnector/ds.h + backend/dsconnector/dsdiscovery.cpp backend/dsconnector/dsdiscovery.h + backend/dsconnector/dsmanager.cpp backend/dsconnector/dsmanager.h + backend/dsconnector/tcpdatatypes.h ) target_link_libraries(qtuiviewerlib PRIVATE @@ -61,6 +69,7 @@ target_link_libraries(qtuiviewerlib PRIVATE Qt6::Multimedia Qt6::MultimediaWidgets Qt6::Concurrent + Qt6::WebSockets ) target_include_directories(qtuiviewerlib PUBLIC @@ -76,5 +85,7 @@ set_property(TARGET ${PROJECT_NAME} PROPERTY QT_ANDROID_EXTRA_LIBS ${ANDROID_OPENSSL_PATH}/libssl_3.so ) -set_property(TARGET ${PROJECT_NAME} PROPERTY QT_ANDROID_TARGET_SDK_VERSION 34) -qt6_import_qml_plugins(${PROJECT_NAME}) +# this needs to be increased with every new release +set(GOOGLE_PLAY_APP_VERSION 27) +# CMAKE_VAR_GIT_VERSION (coming from the top-level CMakeLists.txt) and GOOGLE_PLAY_APP_VERSION replaced in the following file +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/AndroidManifest.xml.in ${CMAKE_CURRENT_SOURCE_DIR}/android/AndroidManifest.xml) diff --git a/src/android/AndroidManifest.xml b/src/android/AndroidManifest.xml.in similarity index 94% rename from src/android/AndroidManifest.xml rename to src/android/AndroidManifest.xml.in index 0f8638c..eec6de3 100644 --- a/src/android/AndroidManifest.xml +++ b/src/android/AndroidManifest.xml.in @@ -1,6 +1,7 @@ <?xml version="1.0"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.qt.qtuiviewer" - android:installLocation="auto" android:versionCode="27" android:versionName="1.0.0"> + android:installLocation="auto" android:versionCode="@GOOGLE_PLAY_APP_VERSION@" + android:versionName="@CMAKE_VAR_GIT_VERSION@"> <!-- %%INSERT_PERMISSIONS --> <!-- %%INSERT_FEATURES --> <supports-screens android:anyDensity="true" android:largeScreens="true" diff --git a/src/backend/backend.cpp b/src/backend/backend.cpp index 79a35b8..6cbbec2 100644 --- a/src/backend/backend.cpp +++ b/src/backend/backend.cpp @@ -24,46 +24,82 @@ ****************************************************************************/ #include "backend.h" +#include "backend/constants.h" #include <QDesktopServices> #include <QEventLoop> #include <QFileInfo> #include <QGuiApplication> +#include <QJniObject> #include <QJsonArray> #include <QJsonObject> #include <QSettings> +#include <QSysInfo> +#include <QTimer> + +// Q_DECLARE_JNI_CLASS(Secure, "android/provider/Settings$Secure"); +// Q_DECLARE_JNI_CLASS(String, "java/lang/String"); +// Q_DECLARE_JNI_CLASS(ContentResolver, "android/content/ContentResolver"); Backend::Backend(QObject *parent) : QObject(parent) { // This will allow us to open the app with the QR code QDesktopServices::setUrlHandler("qtdesignviewer", this, "parseDesignViewerUrl"); + QDesktopServices::setUrlHandler("qtdesignstudio", this, "parseDesignViewerUrl"); QDesktopServices::setUrlHandler("https", this, "parseDesignViewerUrl"); - connect(&m_dsConnectorThread, - &QThread::started, - this, - &Backend::initDesignStudioConnector, - Qt::DirectConnection); - m_dsConnectorThread.start(); - connect(&m_serviceConnector, - &ServiceConnector::downloadProgress, - this, - &Backend::downloadProgress); - - updateUserProjectList(); // Initialize background update - connect(&m_backgroundTimer, &QTimer::timeout, this, &Backend::updateUserProjectList); - m_backgroundTimer.setInterval(1000 * 10); - enableBackgroundUpdate(updateInBackground()); + connect(&m_projectListUpdateTimer, &QTimer::timeout, this, &Backend::updateUserProjectList); + m_projectListUpdateTimer.setInterval(1000 * 10); + m_projectListUpdateTimer.start(); + updateUserProjectList(); + initDesignStudioManager(); + + connect(qApp, + &QGuiApplication::applicationStateChanged, + this, + [this](Qt::ApplicationState state) { + qDebug() << "Application state changed to:" << state; + if (state == Qt::ApplicationState::ApplicationActive) { + m_dsConnectorThread.start(); + m_projectListUpdateTimer.start(); + } else if (state == Qt::ApplicationState::ApplicationSuspended) { + m_dsConnectorThread.quit(); + m_projectListUpdateTimer.stop(); + } + }); const QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); + + // Get the unique device ID + auto context = QNativeInterface::QAndroidApplication::context(); + // auto contentResolver = context.callMethod<QtJniTypes::ContentResolver>("getContentResolver"); + // auto androidId = QtJniTypes::Secure::callStaticMethod<jstring>("getString", + // contentResolver, + // QStringLiteral("android_id")); + // const QByteArray serial = androidId.toString().toLatin1(); + qDebug() << "Qt Design Viewer"; qDebug() << "System information:"; qDebug() << "-- Qt version: " << QT_VERSION_STR; qDebug() << "-- OpenSSL support: " << QVariant(QSslSocket::supportsSsl()).toString(); qDebug() << "-- Screen height: " << QString::number(screenGeometry.height()); qDebug() << "-- Screen width: " << QString::number(screenGeometry.width()); + qDebug() << "-- OS: " << QSysInfo::prettyProductName(); + qDebug() << "-- OS version: " << QSysInfo::productVersion(); + qDebug() << "-- Architecture: " << QSysInfo::currentCpuArchitecture(); + qDebug() << "-- Boot ID: " << QSysInfo::bootUniqueId(); + qDebug() << "-- Kernel type: " << QSysInfo::kernelType(); + qDebug() << "-- Kernel version: " << QSysInfo::kernelVersion(); + qDebug() << "-- Machine unique ID: " << QSysInfo::machineUniqueId(); + qDebug() << "-- Product type: " << QSysInfo::productType(); + qDebug() << "-- Product version: " << QSysInfo::productVersion(); + qDebug() << "-- Build ABI: " << QSysInfo::buildAbi(); + qDebug() << "-- Build CPU architecture: " << QSysInfo::buildCpuArchitecture(); + // qDebug() << "-- Unique device ID: " << serial; + + qDebug() << "Thread id backend:" << QThread::currentThreadId(); } QString Backend::buildInfo() const @@ -102,7 +138,7 @@ void Backend::setUpdateInBackground(const bool &enabled) void Backend::setAutoScaleProject(const bool &enabled) { - QSettings().setValue("system/autoScaleProject", enabled); + QSettings().setValue(Constants::Settings::ProjectManager::AutoScale, enabled); if (enabled) { qDebug() << "Auto scale project is enabled"; } else { @@ -112,23 +148,18 @@ void Backend::setAutoScaleProject(const bool &enabled) void Backend::setUserHash(const QString &userHash) { - QSettings().setValue("user/hash", userHash); + QSettings().setValue(Constants::Settings::Backend::UserHash, userHash); emit userHashChanged(); } -bool Backend::updateInBackground() -{ - return QSettings().value("system/updateInBackground", false).toBool(); -} - bool Backend::autoScaleProject() { - return QSettings().value("system/autoScaleProject", true).toBool(); + return QSettings().value(Constants::Settings::ProjectManager::AutoScale, true).toBool(); } QString Backend::userHash() { - return QSettings().value("user/hash").toString(); + return QSettings().value(Constants::Settings::Backend::UserHash).toString(); } void Backend::updatePopup(const QString &text, bool indeterminate) @@ -138,81 +169,65 @@ void Backend::updatePopup(const QString &text, bool indeterminate) QEventLoop().processEvents(QEventLoop::AllEvents, 1000); } -void Backend::scanQrCode() -{ - m_qrScanner.reset(new QrScanner); - connect(m_qrScanner.data(), &QrScanner::qrCodeScanned, this, [&](const QString &qrCode) { - qDebug() << "QR code scanned:" << qrCode; - parseDesignViewerUrl(QUrl(qrCode)); - m_qrScanner.reset(); - }); - m_qrScanner->scanQrCode(); -} - void Backend::initializeProjectManager() { - m_projectManager.reset(new ProjectManager(autoScaleProject())); - connect(m_projectManager.data(), &ProjectManager::closingProject, this, [&] { + m_projectManager.reset(new ProjectManager); + connect(m_projectManager.get(), &ProjectManager::closingProject, this, [&] { emit popupClose(); m_projectManager.reset(); }); } -void Backend::initDesignStudioConnector() +void Backend::initDesignStudioManager() { - m_designStudioConnector.reset(new DesignStudioConnector); - - connect(m_designStudioConnector.data(), - &DesignStudioConnector::networkStatusUpdated, - this, - &Backend::networkUpdated); - - connect(m_designStudioConnector.data(), &DesignStudioConnector::projectIncoming, this, [&] { - qDebug() << "Project incoming from Design Studio"; - m_projectManager.reset(); - emit popupOpen(); - updatePopup("Receiving project..."); + connect(&m_dsConnectorThread, &QThread::started, [this] { + qDebug() << "Design Studio Manager thread started"; + m_designStudioManager.reset(new DesignStudioManager); + + // signals goes from DS Manager to UI + connect(m_designStudioManager.get(), + &DesignStudioManager::registrationPinRequested, + this, + &Backend::pinRequested); + + connect(m_designStudioManager.get(), + &DesignStudioManager::projectReceived, + this, + &Backend::runDsProject); + + // signals goes from UI to DS Manager + connect(this, + &Backend::enterPin, + m_designStudioManager.get(), + &DesignStudioManager::enterPin); }); - connect( - m_designStudioConnector.data(), - &DesignStudioConnector::projectReceived, - this, - [this](const QByteArray &projectData) { - qDebug() << "Project received from Design Studio"; - initializeProjectManager(); - emit popupOpen(); - updatePopup("Unpacking project..."); - qDebug() << "Project data size: " << projectData.size(); - const QString projectPath = m_projectManager->unpackProject(projectData); - - if (projectPath.isEmpty()) { - qCritical() << "Could not unpack project. Please check the logs for more " - "information."; - emit popupClose(); - return; - } - - qDebug() << "Project unpacked to " << projectPath; - updatePopup("Running project..."); + connect(&m_dsConnectorThread, &QThread::finished, this, [this] { + qDebug() << "Design Studio Manager thread finished"; + m_designStudioManager.reset(); + }); - qDebug() << "Project received confirmation sent to Design Studio"; - if (!m_projectManager->runProject(projectPath)) { - qCritical() << "Could not run project. Please check the logs for more " - "information."; - } else { - m_projectManager->showAppWindow(); - } + connect(&m_serviceConnector, + &ServiceConnector::downloadProgress, + this, + &Backend::downloadProgress); +} - QMetaObject::invokeMethod(m_designStudioConnector.data(), - "sendProjectReceived", - Qt::QueuedConnection); +void Backend::runDsProject(const QByteArray &projectData) +{ + initializeProjectManager(); + emit popupOpen(); + updatePopup("Unpacking project..."); + QString projectPath = m_projectManager->unpackProject(projectData); + updatePopup("Running project..."); - emit popupClose(); - }, - Qt::QueuedConnection); + if (!m_projectManager->runProject(projectPath)) + qCritical() << "Could not run project. Please check the logs for more information."; + else { + m_projectManager->showAppWindow(); + } - qDebug() << "Design Studio Connector is initialized"; + emit popupClose(); } void Backend::runDemoProject(const QString &projectName) @@ -379,7 +394,6 @@ void Backend::updateUserProjectList() const QString userHash = Backend::userHash(); if (userHash.isEmpty()) { - qWarning("User hash is not registered"); return; } @@ -416,27 +430,52 @@ void Backend::updateUserProjectList() emit projectListChanged(); } +void Backend::scanQrCode() +{ + m_qrScanner.reset(new QrScanner); + connect(m_qrScanner.get(), &QrScanner::qrCodeScanned, this, [&](const QString &qrCode) { + qDebug() << "QR code scanned:" << qrCode; + parseDesignViewerUrl(QUrl(qrCode)); + m_qrScanner.reset(); + }); + m_qrScanner->scanQrCode(); +} + void Backend::parseDesignViewerUrl(const QUrl &url) { - QString urlData = url.toString(); - // urlData could be either a direct url to the project or a user hash - // If it is a user hash, we register the user and fetch the project list - // If it is a project url, we submit the url to the text field - // sample url: https://<url>/<project_name>.qmlrc - // sample user hash: qtdesignviewer://19f8907b6t84029384hs8djshdu38476 - if (urlData.isEmpty()) - return; - else if (urlData.startsWith("https//")) { - urlData.replace("https//", "https://"); - emit urlUpdated(urlData); - } else if (urlData.startsWith("https://")) { - emit urlUpdated(urlData); - } else if (urlData.startsWith("qtdesignviewer://")) { + // url format could be one of the following: + // - https://<url>/<project_name>.qmlrc + // - qtdesignviewer://<user_hash> + // - qtdesignstudio://<ipv4_address>?<design_studio_id> + + if (url.scheme() == "https") { + emit urlUpdated(url.toString()); + } else if (url.scheme() == "qtdesignviewer") { qDebug() << "Registering user from QR code"; - setUserHash(url.toString().remove("qtdesignviewer://")); + setUserHash(url.host()); updateUserProjectList(); + } else if (url.scheme() == "qtdesignstudio") { + qDebug() << "Connecting to Design Studio from QR code"; + + if (url.host().isEmpty()) { + qWarning() << "No Design Studio IP address found in the QR code"; + return; + } else if (url.query().isEmpty()) { + qWarning() << "No Design Studio ID found in the QR code"; + return; + } + + const QString ipv4Addr = url.host(); + const QString designStudioId = url.query(); + + QMetaObject::invokeMethod(m_designStudioManager.get(), + "designStudioFound", + Qt::QueuedConnection, + Q_ARG(QString, ipv4Addr), + Q_ARG(QString, designStudioId)); } else { - qWarning() << "Unknown QR code data: " << urlData; + qWarning() << "Unknown QR code format"; + qWarning() << "URL:" << url.toString(); } } diff --git a/src/backend/backend.h b/src/backend/backend.h index dd8b563..38cc89c 100644 --- a/src/backend/backend.h +++ b/src/backend/backend.h @@ -27,11 +27,13 @@ #define DV_ANDROID_H #include <QThread> +#include <QTimer> -#include "dsconnector.h" +#include "dsconnector/dsmanager.h" #include "projectmanager.h" #include "qrscanner.h" #include "serviceconnector.h" +#include <memory> class Backend : public QObject { @@ -57,12 +59,12 @@ private: // Other members ServiceConnector m_serviceConnector; QThread m_dsConnectorThread; - QScopedPointer<ProjectManager> m_projectManager; - QScopedPointer<DesignStudioConnector> m_designStudioConnector; - QScopedPointer<QrScanner> m_qrScanner; + std::unique_ptr<ProjectManager> m_projectManager; + std::unique_ptr<DesignStudioManager> m_designStudioManager; + std::unique_ptr<QrScanner> m_qrScanner; // Settings - QTimer m_backgroundTimer; + QTimer m_projectListUpdateTimer; // member functions void updatePopup(const QString &text, bool indeterminate = true); @@ -83,8 +85,13 @@ signals: void popupOpen(); void popupClose(); - // UI signals - Network page - void networkUpdated(QString); + // UI signals - from DS Manager page + void pinRequested(const QString &id); + void pinPopupOpen(); + + // UI signals - from UI + void enterPin(const QString &deviceId, const QString &pin); + void connectToDesignStudio(const QString &url); public slots: QString buildInfo() const; @@ -93,19 +100,18 @@ public slots: void runOnlineProject(const QString &url); void runUserProject(const QString &projectName, const QString &password); void runDemoProject(const QString &projectName); + void runDsProject(const QByteArray &projectData); void clearDemoCaches(); - void initDesignStudioConnector(); + void initDesignStudioManager(); void parseDesignViewerUrl(const QUrl &url); // settings - setters - void setUpdateInBackground(const bool &enabled); void setAutoScaleProject(const bool &enabled); void setUserHash(const QString &userHash); // settings - getters - bool updateInBackground(); bool autoScaleProject(); QString userHash(); @@ -113,7 +119,6 @@ public slots: private slots: void initializeProjectManager(); - void enableBackgroundUpdate(const bool &enabled); void updateUserProjectList(); }; diff --git a/src/backend/constants.h b/src/backend/constants.h new file mode 100644 index 0000000..540341e --- /dev/null +++ b/src/backend/constants.h @@ -0,0 +1,54 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QStandardPaths> +#include <QString> + +namespace Constants { + +// Settings keys +namespace Settings { +namespace ProjectManager { +const static QString AutoScale = "project/autoScale"; +} // namespace ProjectManager + +namespace Backend { +const static QString UserHash = "user/hash"; +} // namespace Backend +} // namespace Settings + +// Path Constants +namespace Paths { +const static QString WritableLocation = QStandardPaths::writableLocation( + QStandardPaths::AppDataLocation); +const static QString ProjectCachePath = WritableLocation + "/projectCache"; +const static QString DemoProjectsPath = WritableLocation + "/demoProjects"; +const static QString ConfigPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); +const static QString ConfigPathDsManager = ConfigPath + "/dsmanager.json"; +} // namespace Paths + +} // namespace Constants diff --git a/src/backend/dsconnector.cpp b/src/backend/dsconnector.cpp deleted file mode 100644 index b9f46eb..0000000 --- a/src/backend/dsconnector.cpp +++ /dev/null @@ -1,128 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2023 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Design Viewer of the Qt Toolkit. -** -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -****************************************************************************/ - -#include "dsconnector.h" - -#include <QNetworkInterface> - -DesignStudioConnector::DesignStudioConnector(const quint16 port, QObject *parent) - : QObject(parent) - , m_tcpPort(port) -{ - initTcpServer(); -} - -void DesignStudioConnector::receiveProject() -{ - QByteArray data = m_tcpSocket->readAll(); - - if (data.startsWith("qres")) { - qDebug() << "TCP:: Received project start delimeter"; - m_projectData.clear(); - m_projectData.append(data); - m_receivingData = true; - emit projectIncoming(); - } else if (m_receivingData) { - if (data.contains("::qmlrc-end::")) { - qDebug() << "TCP:: Received project end delimeter"; - m_projectData.append(data.mid(0, data.size() - 13)); - emit projectReceived(m_projectData); - m_receivingData = false; - } else { - qDebug() << "TCP:: Received data sequence"; - m_projectData.append(data); - } - } else { - qDebug() << "TCP:: Received unknown data:" << data; - } -} - -void DesignStudioConnector::initTcpServer() -{ - const bool retVal = m_tcpServer.listen(QHostAddress::Any, m_tcpPort); - - updateIpv4Addr(); - - if (!retVal) { - qDebug() << "Failed to listen on port " << m_tcpPort; - emit networkStatusUpdated("Failed to bind on port " + QString::number(m_tcpPort)); - return; - } - - qDebug() << "Listening on port " << m_tcpPort; - connect(&m_tcpServer, &QTcpServer::newConnection, this, &DesignStudioConnector::clientConnected); - - m_ipUpdateTimer.setInterval(5000); - connect(&m_ipUpdateTimer, &QTimer::timeout, this, &DesignStudioConnector::updateIpv4Addr); - m_ipUpdateTimer.start(); -} - -void DesignStudioConnector::clientConnected() -{ - qDebug() << "New connection from Design Studio"; - emit networkStatusUpdated("Qt Design Studio is connected.\n Waiting for project..."); - m_tcpSocket.reset(m_tcpServer.nextPendingConnection()); - m_projectData.clear(); - m_ipUpdateTimer.stop(); - - connect(m_tcpSocket.data(), &QTcpSocket::disconnected, this, [&]() { - qDebug() << "Disconnected from Design Studio"; - emit networkStatusUpdated("\nLocal IP: " + m_ipv4Addr - + "\nWaiting for Qt Design Studio to connect..."); - }); - - connect(m_tcpSocket.data(), - &QTcpSocket::readyRead, - this, - &DesignStudioConnector::receiveProject); -} - -void DesignStudioConnector::updateIpv4Addr() -{ - const QList<QHostAddress> list = QNetworkInterface::allAddresses(); - for (const QHostAddress &address : list) { - if (address.protocol() == QAbstractSocket::IPv4Protocol - && address != QHostAddress::LocalHost) { - if (m_ipv4Addr != address.toString()) { - qDebug() << "Local IP: " << address.toString(); - } - m_ipv4Addr = address.toString(); - break; - } - } - emit networkStatusUpdated("Local IP: " + m_ipv4Addr - + "\nWaiting for Design Studio to connect..."); -} - -void DesignStudioConnector::sendProjectReceived() -{ - if (!m_tcpSocket) { - qDebug() << "TCP:: Socket is not connected"; - return; - } - - m_tcpSocket->write("::qmlrc-received::"); - m_tcpSocket->waitForBytesWritten(3000); -} diff --git a/src/backend/dsconnector/ds.cpp b/src/backend/dsconnector/ds.cpp new file mode 100644 index 0000000..a3ca3e9 --- /dev/null +++ b/src/backend/dsconnector/ds.cpp @@ -0,0 +1,236 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "ds.h" + +#include <QDebug> +#include <QGuiApplication> +#include <QJsonDocument> +#include <QJsonObject> +#include <QRect> +#include <QScreen> +#include <QSysInfo> +#include <QTimer> + +#include "tcpdatatypes.h" + +DesignStudio::DesignStudio(const QString &ipv4Addr, + const quint16 port, + const QString &designStudioId, + const QString &deviceId, + QObject *parent) + : QObject(parent) + , m_ipv4Addr(ipv4Addr) + , m_port(port) + , m_url("ws://" + m_ipv4Addr + ":" + QString::number(m_port)) + , m_designStudioId(designStudioId) + , m_deviceId(deviceId) +{ + initPingPong(); + initSocket(); +} + +void DesignStudio::initPingPong() +{ + connect(&m_pingTimer, &QTimer::timeout, this, [this]() { + m_socket.ping(); + m_pongTimer.start(5000); + }); + + connect(&m_socket, &QWebSocket::pong, this, [this](quint64 elapsedTime, const QByteArray &) { + qDebug() << "Pong received from Design Studio" << m_designStudioId << "in" << elapsedTime + << "ms"; + m_pongTimer.stop(); + }); + + connect(&m_pongTimer, &QTimer::timeout, this, [this]() { + qDebug() << "Design Studio" << m_designStudioId << "is not responding. Reconnecting."; + m_socket.close(); + }); +} + +void DesignStudio::initSocket() +{ + connect(&m_socket, &QWebSocket::textMessageReceived, this, &DesignStudio::processTextMessage); + connect(&m_socket, + &QWebSocket::binaryMessageReceived, + this, + &DesignStudio::processBinaryMessage); + connect(&m_socket, &QWebSocket::disconnected, this, [this]() { + QTimer::singleShot(m_reconnectTimeout, this, [this]() { m_socket.open(m_url); }); + }); + + connect(&m_socket, &QWebSocket::stateChanged, this, [this](QAbstractSocket::SocketState state) { + if (state == QAbstractSocket::ConnectedState) { + qDebug() << "Connected to Design Studio" << m_designStudioId; + m_socketWasConnected = true; + m_pingTimer.start(10000); + m_pongTimer.stop(); + } else if (state == QAbstractSocket::UnconnectedState && m_socketWasConnected) { + qDebug() << "Disconnected from Design Studio" << m_designStudioId; + m_socketWasConnected = false; + m_pingTimer.stop(); + m_pongTimer.stop(); + } + }); + + m_socket.open(m_url); +} + +QString DesignStudio::ipv4Addr() const +{ + return m_ipv4Addr; +} + +quint16 DesignStudio::port() const +{ + return m_port; +} + +QUrl DesignStudio::url() const +{ + return m_url; +} + +QString DesignStudio::designStudioId() const +{ + return m_designStudioId; +} + +QString DesignStudio::deviceId() const +{ + return m_deviceId; +} + +bool DesignStudio::isRegistered() const +{ + return !m_deviceId.isEmpty(); +} + +bool DesignStudio::connected() const +{ + return m_socket.state() == QAbstractSocket::ConnectedState; +} + +void DesignStudio::setIpAddress(const QString &ipv4Addr) +{ + m_socket.close(); + m_ipv4Addr = ipv4Addr; + m_url = QUrl("ws://" + m_ipv4Addr + ":" + QString::number(m_port)); + m_socket.open(m_url); +} + +void DesignStudio::sendDeviceInfo() +{ + const QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); + + QJsonObject deviceInfo; + deviceInfo["screenHeight"] = screenGeometry.height(); + deviceInfo["screenWidth"] = screenGeometry.width(); + deviceInfo["os"] = QSysInfo::prettyProductName(); + deviceInfo["osVersion"] = QSysInfo::productVersion(); + deviceInfo["architecture"] = QSysInfo::currentCpuArchitecture(); + deviceInfo["deviceId"] = m_deviceId; + deviceInfo["appVersion"] = QString(CMAKE_VAR_GIT_VERSION); + + qDebug() << "Sending device info to Design Studio" << deviceInfo; + + sendData(PackageToDesignStudio::deviceInfo, deviceInfo); +} + +void DesignStudio::sendRegistrationPin(const QString &pin) +{ + qDebug() << "Sending registration PIN to Design Studio with pin" << pin; + sendData(PackageToDesignStudio::registrationPin, pin); +} + +void DesignStudio::sendData(const QLatin1String &dataType, const QJsonValue &data) +{ + QJsonObject message; + message["dataType"] = dataType; + message["data"] = data; + + m_socket.sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact)); +} + +void DesignStudio::processTextMessage(const QString &message) +{ + QJsonParseError jsonError; + const QJsonDocument jsonDoc = QJsonDocument::fromJson(message.toLatin1(), &jsonError); + + if (jsonError.error != QJsonParseError::NoError) { + qDebug() << "Failed to parse JSON message:" << jsonError.errorString() << message; + return; + } + + const QJsonObject jsonObj = jsonDoc.object(); + if (!jsonObj.contains("dataType")) { + qDebug() << "Invalid JSON message:" << jsonObj; + return; + } + + const QString dataType = jsonObj.value("dataType").toString(); + + if (dataType == PackageFromDesignStudio::designStudioReady) { + m_designStudioId = jsonObj.value("data").toString(); + qDebug() << "Design Studio" << m_designStudioId << "is waiting for the info."; + sendDeviceInfo(); + } else if (dataType == PackageFromDesignStudio::pinVerificationRequested) { + qDebug() << "Registration PIN requested by Design Studio"; + emit registrationPinRequested(m_designStudioId); + } else if (dataType == PackageFromDesignStudio::pinVerificationFailed) { + qDebug() << "Registration PIN verification failed"; + emit pinVerificationFailed(m_designStudioId); + } else if (dataType == PackageFromDesignStudio::deviceIdReceived) { + if (!m_deviceId.isEmpty()) { + qDebug() << "Registration ID already set. Ignoring new registration ID"; + return; + } + + m_deviceId = jsonObj.value("data").toString(); + qDebug() << "Registration successful. Received ID:" << m_deviceId; + emit registrationSucceeded(m_designStudioId); + emit dsOnline(m_designStudioId, m_ipv4Addr); + } else if (dataType == PackageFromDesignStudio::deviceIdDeclined) { + qDebug() << "Design Studio unregistered"; + m_deviceId.clear(); + emit unregistered(m_designStudioId); + } else if (dataType == PackageFromDesignStudio::deviceIdAccepted) { + qDebug() << "Design Studio accepted the device ID. Connection alive."; + emit dsOnline(m_designStudioId, m_ipv4Addr); + } else if (dataType == PackageFromDesignStudio::projectData) { + qDebug() << "Project is expected"; + emit projectIncoming(); + } else { + qDebug() << "Unkown JSON message"; + } +} + +void DesignStudio::processBinaryMessage(const QByteArray &data) +{ + // this slot is only triggered when we're receiving a qmlrc project + qDebug() << "Binary message received (most probably a project)"; + emit projectReceived(data); +} diff --git a/src/backend/dsconnector/ds.h b/src/backend/dsconnector/ds.h new file mode 100644 index 0000000..6c39361 --- /dev/null +++ b/src/backend/dsconnector/ds.h @@ -0,0 +1,99 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QObject> +#include <QTimer> +#include <QWebSocket> +#include <qjsonvalue.h> +#include <qlatin1stringview.h> + +class DesignStudio : public QObject +{ + Q_OBJECT +public: + DesignStudio(const QString &ipv4Addr, + const quint16 port, + const QString &designStudioId = QString(), + const QString &deviceId = QString(), + QObject *parent = nullptr); + + // Getters + QString ipv4Addr() const; + QString designStudioId() const; + quint16 port() const; + QUrl url() const; + QString deviceId() const; + bool isRegistered() const; + bool connected() const; + + // Setters + void setIpAddress(const QString &ipv4Addr); + + // Send data + void sendRegistrationPin(const QString &pin); + +private: + // Network + QWebSocket m_socket; + QString m_ipv4Addr; + quint16 m_port; + QUrl m_url; + bool m_socketWasConnected = false; + QTimer m_pingTimer; + QTimer m_pongTimer; + + // DS data + QString m_designStudioId; + QString m_deviceId; + + // Settings + constexpr static int m_reconnectTimeout = 5000; + + // DS comm + void sendDeviceInfo(); + void sendData(const QLatin1String &dataType, const QJsonValue &data = QJsonValue()); + + // Internal + void initPingPong(); + void initSocket(); + +private slots: + void processTextMessage(const QString &data); + void processBinaryMessage(const QByteArray &data); + +signals: + // DS signals + void registrationPinRequested(const QString &id); + void pinVerificationFailed(const QString &id); + void registrationSucceeded(const QString &id); + void unregistered(const QString &id); + + void dsOnline(const QString &id, const QString &ipv4Addr); + + void projectIncoming(); + void projectReceived(const QByteArray &data); +}; diff --git a/src/backend/dsconnector/dsdiscovery.cpp b/src/backend/dsconnector/dsdiscovery.cpp new file mode 100644 index 0000000..9c0fcc5 --- /dev/null +++ b/src/backend/dsconnector/dsdiscovery.cpp @@ -0,0 +1,86 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "dsdiscovery.h" + +#include <QNetworkDatagram> +#include <QNetworkInterface> + +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonParseError> +#include <qjsondocument.h> + +DesignStudioDiscovery::DesignStudioDiscovery(QObject *parent) + : QObject(parent) +{ + const QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces(); + for (const QNetworkInterface &interface : interfaces) { + if (interface.flags().testFlag(QNetworkInterface::IsUp) + && interface.flags().testFlag(QNetworkInterface::IsRunning) + && !interface.flags().testFlag(QNetworkInterface::IsLoopBack)) { + for (const QNetworkAddressEntry &entry : interface.addressEntries()) { + if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) { + const bool retVal = m_udpSocket.bind(QHostAddress::AnyIPv4, + 53452, + QUdpSocket::ShareAddress); + if (!retVal) { + qWarning() << "UDP:: Failed to bind to port 53452"; + } + } + } + } + } + + qDebug() << "UDP:: Listening on" << m_udpSocket.localAddress() << "port" + << m_udpSocket.localPort(); + connect(&m_udpSocket, &QUdpSocket::readyRead, this, &DesignStudioDiscovery::onReadyRead); +} + +void DesignStudioDiscovery::onReadyRead() +{ + while (m_udpSocket.hasPendingDatagrams()) { + const QNetworkDatagram datagram = m_udpSocket.receiveDatagram(); + const QString message = datagram.data(); + const QString ipv4Addr = datagram.senderAddress().toString(); + + QJsonParseError jsonError; + const QJsonDocument jsonDoc = QJsonDocument::fromJson(message.toUtf8(), &jsonError); + + if (jsonError.error != QJsonParseError::NoError) { + qDebug() << "UDP:: Failed to parse JSON message:" << jsonError.errorString(); + continue; + } + + const QJsonObject jsonObj = jsonDoc.object(); + + if (!jsonObj.contains("name") && !jsonObj.contains("id")) { + qDebug() << "UDP:: Invalid JSON message:" << jsonObj; + continue; + } + + emit designStudioFound(ipv4Addr, jsonObj.value("id").toString()); + } +} diff --git a/src/backend/dsconnector.h b/src/backend/dsconnector/dsdiscovery.h similarity index 58% rename from src/backend/dsconnector.h rename to src/backend/dsconnector/dsdiscovery.h index 1068224..369e924 100644 --- a/src/backend/dsconnector.h +++ b/src/backend/dsconnector/dsdiscovery.h @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2023 The Qt Company Ltd. +** Copyright (C) 2024 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt Design Viewer of the Qt Toolkit. @@ -23,48 +23,23 @@ ** ****************************************************************************/ -#ifndef DSCONNECTOR_H -#define DSCONNECTOR_H +#pragma once #include <QObject> -#include <QTcpServer> -#include <QTcpSocket> -#include <QTimer> #include <QUdpSocket> -class DesignStudioConnector : public QObject +class DesignStudioDiscovery : public QObject { Q_OBJECT public: - explicit DesignStudioConnector(const quint16 port = 40000, QObject *parent = nullptr); + explicit DesignStudioDiscovery(QObject *parent = nullptr); -public slots: - void sendProjectReceived(); - -private: - // Tcp connection members - QTcpServer m_tcpServer; - QScopedPointer<QTcpSocket> m_tcpSocket; - const quint32 m_tcpPort; - - // Udp connection members - QTimer m_ipUpdateTimer; - QString m_ipv4Addr; - - // Other members - QByteArray m_projectData; - bool m_receivingData; - - // Member functions - void initTcpServer(); - void receiveProject(); - void updateIpv4Addr(); - void clientConnected(); +private slots: + void onReadyRead(); signals: - void networkStatusUpdated(QString); - void projectReceived(QByteArray); - void projectIncoming(); -}; + void designStudioFound(const QString &ipv4Addr, const QString &id); -#endif // DSCONNECTOR_H +private: + QUdpSocket m_udpSocket; +}; diff --git a/src/backend/dsconnector/dsmanager.cpp b/src/backend/dsconnector/dsmanager.cpp new file mode 100644 index 0000000..ebbfe21 --- /dev/null +++ b/src/backend/dsconnector/dsmanager.cpp @@ -0,0 +1,187 @@ +/**************************************************************************** +** +** Copyright (C) 2023 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "dsmanager.h" +#include "../constants.h" +#include "backend/dsconnector/ds.h" +#include <memory> + +#include <QFile> +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> +#include <QLoggingCategory> +#include <QThread> +#include <qdebug.h> +#include <qjsonobject.h> +#include <qobject.h> + +DesignStudioManager::DesignStudioManager(QObject *parent) + : QObject(parent) +{ + initializeRegisteredDesignStudios(); + + m_discovery = std::make_unique<DesignStudioDiscovery>(); + connect(m_discovery.get(), + &DesignStudioDiscovery::designStudioFound, + this, + &DesignStudioManager::designStudioFound); + + qDebug() << "Design Studio Manager initialized"; + qDebug() << "Thread id dsmanger:" << QThread::currentThreadId(); +} + +void DesignStudioManager::initializeRegisteredDesignStudios() +{ + QFile file(Constants::Paths::ConfigPathDsManager); + + if (!file.exists()) { + qDebug() << "Settings file not found."; + return; + } + + file.open(QIODevice::ReadOnly); + QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + file.close(); + + if (!doc.isObject()) { + qDebug() << "Settings file is not a JSON object."; + return; + } + + QJsonObject obj = doc.object(); + QJsonArray dsArray = obj["designStudios"].toArray(); + for (const auto &ds : dsArray) { + QJsonObject dsObj = ds.toObject(); + initDesignStudio(dsObj["ipv4Addr"].toString(), + dsObj["designStudioId"].toString(), + dsObj["deviceId"].toString()); + } +} + +void DesignStudioManager::initDesignStudio(const QString &ipv4Addr, + const QString &designStudioId, + const QString &deviceId) +{ + qDebug() << "Initializing Design Studio" << designStudioId << "with IPv4 address" << ipv4Addr; + auto ds = std::make_unique<DesignStudio>(ipv4Addr, 40000, designStudioId, deviceId); + + connect(ds.get(), + &DesignStudio::registrationSucceeded, + this, + &DesignStudioManager::designStudioRegistered); + + connect(ds.get(), + &DesignStudio::unregistered, + this, + &DesignStudioManager::designStudioUnregistered); + + connect(ds.get(), + &DesignStudio::registrationPinRequested, + this, + &DesignStudioManager::registrationPinRequested); + + connect(ds.get(), &DesignStudio::projectReceived, this, &DesignStudioManager::projectReceived); + + m_designStudios.push_back(std::move(ds)); +} + +void DesignStudioManager::designStudioFound(const QString &ipv4Addr, const QString &id) +{ + qDebug() << "Design Studio found with IPv4 address" << ipv4Addr << "and ID" << id; + // check if the Design Studio is already in the list + + for (const auto &ds : m_designStudios) { + if (ds->designStudioId() == id) { + if (ds->ipv4Addr() != ipv4Addr) { + qDebug() << "Design Studio" << id << "changed IP address from" << ds->ipv4Addr() + << "to" << ipv4Addr; + ds->setIpAddress(ipv4Addr); + } else { + qDebug() << "Design Studio" << id << "already in the list"; + } + return; + } + } + + initDesignStudio(ipv4Addr, id); +} + +void DesignStudioManager::updateConfigFile() +{ + QFile file(Constants::Paths::ConfigPathDsManager); + if (!file.open(QIODevice::ReadWrite | QIODevice::Truncate)) { + qDebug() << "Failed to open settings file for writing"; + return; + } + + QJsonObject rootObj; + QJsonArray dsArray; + + for (const auto &d : m_designStudios) { + if (!d->isRegistered()) { + continue; + } + + QJsonObject dsObj; + dsObj["ipv4Addr"] = d->ipv4Addr(); + dsObj["designStudioId"] = d->designStudioId(); + dsObj["deviceId"] = d->deviceId(); + dsArray.append(dsObj); + } + + rootObj["designStudios"] = dsArray; + file.write(QJsonDocument(rootObj).toJson()); +} + +void DesignStudioManager::designStudioRegistered(const QString &designStudioId) +{ + qDebug() << "Design Studio" << designStudioId << "registered. Updating settings."; + updateConfigFile(); +} + +void DesignStudioManager::designStudioUnregistered(const QString &designStudioId) +{ + qDebug() << "Design Studio" << designStudioId << "unregistered. Removing from settings."; + updateConfigFile(); +} + +void DesignStudioManager::enterPin(const QString &id, const QString &pin) +{ + DesignStudio *ds = nullptr; + + for (const auto &d : m_designStudios) { + if (d->designStudioId() == id) { + ds = d.get(); + break; + } + } + + if (!ds) { + return; + } + + ds->sendRegistrationPin(pin); +} diff --git a/src/backend/dsconnector/dsmanager.h b/src/backend/dsconnector/dsmanager.h new file mode 100644 index 0000000..824996d --- /dev/null +++ b/src/backend/dsconnector/dsmanager.h @@ -0,0 +1,69 @@ +/**************************************************************************** +** +** Copyright (C) 2023 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#ifndef DSCONNECTOR_H +#define DSCONNECTOR_H + +#include <QObject> +#include <qscopedpointer.h> + +#include "ds.h" +#include "dsdiscovery.h" +#include <memory> + +class DesignStudioManager : public QObject +{ + Q_OBJECT +public: + explicit DesignStudioManager(QObject *parent = nullptr); + +public slots: + void enterPin(const QString &id, const QString &pin); + void designStudioFound(const QString &ipv4Addr, const QString &id); + +private slots: + void designStudioRegistered(const QString &id); + void designStudioUnregistered(const QString &id); + +private: + // Discovery object + std::unique_ptr<DesignStudioDiscovery> m_discovery; + + // Discovered Design Studio instances + std::vector<std::unique_ptr<DesignStudio>> m_designStudios; + + void initializeRegisteredDesignStudios(); + void initDesignStudio(const QString &ipv4Addr, + const QString &designStudioId, + const QString &deviceId = QString()); + + void updateConfigFile(); + +signals: + void registrationPinRequested(const QString &id); + void projectReceived(const QByteArray &project); +}; + +#endif // DSCONNECTOR_H diff --git a/src/backend/dsconnector/tcpdatatypes.h b/src/backend/dsconnector/tcpdatatypes.h new file mode 100644 index 0000000..5001db5 --- /dev/null +++ b/src/backend/dsconnector/tcpdatatypes.h @@ -0,0 +1,45 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QLatin1String> + +namespace PackageFromDesignStudio { +using namespace Qt::Literals; +constexpr auto designStudioReady = "designStudioReady"_L1; +constexpr auto pinVerificationRequested = "pinRequested"_L1; +constexpr auto pinVerificationFailed = "pinVerificationFailed"_L1; +constexpr auto deviceIdReceived = "deviceIdReceived"_L1; +constexpr auto deviceIdDeclined = "deviceIdDeclined"_L1; +constexpr auto deviceIdAccepted = "deviceIdAccepted"_L1; +constexpr auto projectData = "projectData"_L1; +}; // namespace PackageFromDesignStudio + +namespace PackageToDesignStudio { +using namespace Qt::Literals; +constexpr auto deviceInfo = "deviceInfo"_L1; +constexpr auto registrationPin = "registrationPinResponse"_L1; +}; // namespace PackageToDesignStudio diff --git a/src/backend/projectmanager.cpp b/src/backend/projectmanager.cpp index ed79581..dbae903 100644 --- a/src/backend/projectmanager.cpp +++ b/src/backend/projectmanager.cpp @@ -37,33 +37,36 @@ #include <QRandomGenerator> #include <QRegularExpression> #include <QResource> -#include <QStandardPaths> +#include <QSettings> #include <QTemporaryDir> #include <QTemporaryFile> + #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) #include <QtCore/private/qzipreader_p.h> #else #include <QtGui/private/qzipreader_p.h> #endif -ProjectManager::ProjectManager(const bool &autoScaleProject, QObject *parent) +#include "constants.h" + +ProjectManager::ProjectManager(QObject *parent) : QObject(parent) - , m_projectCachePath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)) - , m_demoProjectCachePath(m_projectCachePath + "/demoProjects") - , m_autoScaleProject(autoScaleProject) + , m_autoScaleProject( + QSettings().value(Constants::Settings::ProjectManager::AutoScale, true).toBool()) { qDebug() << "ProjectManager created."; - qDebug() << "Project cache path: " << m_projectCachePath; - qDebug() << "Demo project cache path: " << m_demoProjectCachePath; + qDebug() << "Project cache path: " << Constants::Paths::ProjectCachePath; + qDebug() << "Demo project cache path: " << Constants::Paths::DemoProjectsPath; + qDebug() << "Auto scale project: " << m_autoScaleProject; - if (!QDir(m_projectCachePath).exists()) { - qDebug() << "Creating project cache path: " << m_projectCachePath; - QDir().mkpath(m_projectCachePath); + if (!QDir(Constants::Paths::ProjectCachePath).exists()) { + qDebug() << "Creating project cache path: " << Constants::Paths::ProjectCachePath; + QDir().mkpath(Constants::Paths::ProjectCachePath); } - if (!QDir(m_demoProjectCachePath).exists()) { - qDebug() << "Creating demo project cache path: " << m_demoProjectCachePath; - QDir().mkpath(m_demoProjectCachePath); + if (!QDir(Constants::Paths::DemoProjectsPath).exists()) { + qDebug() << "Creating demo project cache path: " << Constants::Paths::DemoProjectsPath; + QDir().mkpath(Constants::Paths::DemoProjectsPath); } } @@ -324,7 +327,7 @@ bool ProjectManager::cacheProject(const QByteArray &projectData, const QJsonObje qDebug() << "Caching project " << projectId << " with last modified " << projectInfo.value("uploadTime"); - const QString cachePath = m_projectCachePath + "/" + projectId; + const QString cachePath = Constants::Paths::ProjectCachePath + "/" + projectId; // remove old cache if (QDir(cachePath).exists()) { @@ -370,7 +373,7 @@ bool ProjectManager::isProjectCached(const QJsonObject &projectInfo) const QString lastModified = projectInfo.value("uploadTime").toString(); qDebug() << "Checking if project " << projectId << " is cached"; - const QString cachePath = m_projectCachePath + "/" + projectId; + const QString cachePath = Constants::Paths::ProjectCachePath + "/" + projectId; if (!QDir(cachePath).exists()) { qDebug() << "Project " << projectId << " is not cached"; return false; @@ -404,7 +407,7 @@ void ProjectManager::clearCachedProject(const QJsonObject &projectInfo) const QString projectId = projectInfo.value("id").toString(); qDebug() << "Clearing cache for project " << projectId; - const QString cachePath = m_projectCachePath + "/" + projectId; + const QString cachePath = Constants::Paths::ProjectCachePath + "/" + projectId; if (!QDir(cachePath).exists()) { qDebug() << "Project " << projectId << " is not cached"; } @@ -418,7 +421,7 @@ bool ProjectManager::runCachedProject(const QJsonObject &projectInfo) const QString projectId = projectInfo.value("id").toString(); qDebug() << "Running cached project " << projectId; - const QString projectPath = m_projectCachePath + "/" + projectId; + const QString projectPath = Constants::Paths::ProjectCachePath + "/" + projectId; return runProject(projectPath); } @@ -427,7 +430,7 @@ bool ProjectManager::cacheDemoProject(const QByteArray &projectData, const QJson // sample project info //[{"lastUpdate":1701947766739.9812,"name":"ClusterTutorial.qmlrc"}] const QString projectName = projectInfo.value("name").toString().remove(".qmlrc"); - const QString demoProjectPath = m_demoProjectCachePath + "/" + projectName; + const QString demoProjectPath = Constants::Paths::DemoProjectsPath + "/" + projectName; qDebug() << "Caching demo project " << projectName << " to " << demoProjectPath; // remove old cache @@ -476,8 +479,8 @@ bool ProjectManager::cacheDemoProject(const QByteArray &projectData, const QJson void ProjectManager::clearDemoCaches() { - qDebug() << "Clearing demo caches"; - QDir(m_demoProjectCachePath).removeRecursively(); + qDebug() << "Clearing demo caches:" << Constants::Paths::DemoProjectsPath; + QDir(Constants::Paths::DemoProjectsPath).removeRecursively(); } bool ProjectManager::isDemoProjectCached(const QJsonObject &projectInfo) @@ -485,7 +488,7 @@ bool ProjectManager::isDemoProjectCached(const QJsonObject &projectInfo) // sample project info //[{"lastUpdate":1701947766739.9812,"name":"ClusterTutorial.qmlrc"}] const QString projectName = projectInfo.value("name").toString().remove(".qmlrc"); - const QString demoProjectPath = m_demoProjectCachePath + "/" + projectName; + const QString demoProjectPath = Constants::Paths::DemoProjectsPath + "/" + projectName; qDebug() << "Checking if demo project " << projectName << " is cached"; if (!QDir(demoProjectPath).exists()) { @@ -520,7 +523,7 @@ bool ProjectManager::isDemoProjectCached(const QJsonObject &projectInfo) bool ProjectManager::runDemoProject(const QString &projectName) { - const QString demoProjectPath = m_demoProjectCachePath + "/" + projectName; + const QString demoProjectPath = Constants::Paths::DemoProjectsPath + "/" + projectName; qDebug() << "Running demo project " << projectName << " from " << demoProjectPath; return runProject(demoProjectPath); } diff --git a/src/backend/projectmanager.h b/src/backend/projectmanager.h index 8c79fac..354ba59 100644 --- a/src/backend/projectmanager.h +++ b/src/backend/projectmanager.h @@ -37,7 +37,7 @@ class ProjectManager : public QObject { Q_OBJECT public: - explicit ProjectManager(const bool &autoScaleProject = true, QObject *parent = nullptr); + explicit ProjectManager(QObject *parent = nullptr); ~ProjectManager(); QString unpackProject(const QByteArray &project, bool extractZip = false); @@ -63,8 +63,6 @@ private: // Member variables QByteArray m_projectData; QString m_projectPath; - const QString m_projectCachePath; - const QString m_demoProjectCachePath; const bool m_autoScaleProject; // Qml related members diff --git a/src/ui/DSManagement.qml b/src/ui/DSManagement.qml new file mode 100644 index 0000000..a73578e --- /dev/null +++ b/src/ui/DSManagement.qml @@ -0,0 +1,35 @@ +import QtQuick +import QtQuick.Controls 6.4 +import QtQuick.Layouts + + +Item { + id: header + + ColumnLayout { + spacing: 10 + anchors.fill: parent + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + Text { + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Layout.preferredWidth: parent.width + horizontalAlignment: "AlignHCenter" + wrapMode: Text.WordWrap + textFormat: Text.StyledText + text: "<p> + Use Design Studio (File -> Device Management) to register you phone. + <br> + + <p> + If your phone is not discovered by Design Studio, or if it has been registered before but doesn't show up as online, you can scan the QR code shown on Design Studio. + " + } + + Button { + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + text: "Scan QR Code" + onClicked: backend.scanQrCode() + } + } +} diff --git a/src/ui/HomePage.qml b/src/ui/HomePage.qml index 428221b..9e15965 100644 --- a/src/ui/HomePage.qml +++ b/src/ui/HomePage.qml @@ -73,41 +73,6 @@ Item { Layout.fillWidth: true Layout.fillHeight: true } - - Text { - id: dsInstructions - text: "Create a connection to Qt Design Studio: - Goto File > Deploy Project to Android, and type in the following IP address:" - font.pixelSize: 12 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - Text { - id: ipAdress - text: "Waiting for backend to be initialized..." - font.pixelSize: 16 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - wrapMode: Text.WordWrap - Layout.fillWidth: true - Connections { - target: backend - function onNetworkUpdated(newStatus){ - ipAdress.text = newStatus - } - } - } - - Item { - id: item5 - Layout.preferredWidth: 10 - Layout.preferredHeight: 10 - Layout.fillWidth: true - Layout.fillHeight: true - } } Item { @@ -265,11 +230,6 @@ Item { target: sep2 visible: false } - - PropertyChanges { - target: item5 - visible: true - } }, State { name: "State2" @@ -295,11 +255,6 @@ Item { visible: true } - PropertyChanges { - target: item5 - visible: false - } - PropertyChanges { target: gridLayout columns: 2 diff --git a/src/ui/Network.qml b/src/ui/Network.qml deleted file mode 100644 index 1fc374c..0000000 --- a/src/ui/Network.qml +++ /dev/null @@ -1,55 +0,0 @@ -import QtQuick -import QtQuick.Controls 6.4 -import QtQuick.Layouts - - -Item { - id: header - - ColumnLayout { - anchors.fill: parent - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - - Item { - id: item2 - width: 200 - height: 200 - Layout.preferredHeight: 10 - Layout.fillWidth: true - } - - Label { - id: infoHeader - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - horizontalAlignment: "AlignHCenter" - text: qsTr("Connect to Qt Design Studio: - \n1. Copy the Local IP. - \n2. Open the project in Qt Design Studio (4.4 or later). - \n3. Select File -> Deploy Project to Android. - \n4. Paste the Local IP to Device IP. - \n5. Select Connect. - \n6. Select Send Project.") - } - - Label { - id: statusLabel - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - horizontalAlignment: "AlignHCenter" - text: "Waiting for backend to be initialized..." - Connections { - target: backend - function onNetworkUpdated(newStatus){ - statusLabel.text = newStatus - } - } - } - - Item { - id: item3 - width: 200 - height: 200 - Layout.preferredHeight: 10 - Layout.fillWidth: true - } - } -} diff --git a/src/ui/SettingsPage.qml b/src/ui/SettingsPage.qml index 894afb5..79cc624 100644 --- a/src/ui/SettingsPage.qml +++ b/src/ui/SettingsPage.qml @@ -20,27 +20,12 @@ Item { id: column2 Layout.fillWidth: true - CheckBox { - id: checkBox - text: qsTr("Update user projects in the backgroud") - font.pointSize: 15 - onCheckStateChanged: backend.setUpdateInBackground(checkState) - checkState: backend.updateInBackground() ? Qt.Checked : Qt.Unchecked - } - - Text{ - leftPadding: 45 - text: qsTr("Checks new projects every 10 seconds") - font.pointSize: 12 - } - CheckBox { id: checkBox2 text: qsTr("Auto scale the project") font.pointSize: 15 onCheckStateChanged: backend.setAutoScaleProject(checkState) checkState: backend.autoScaleProject() ? Qt.Checked : Qt.Unchecked - } Text{ diff --git a/src/ui/content/images/ds_icon.png b/src/ui/content/images/ds_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d565981cdae7b075f7565da7325e893950f75799 GIT binary patch literal 10212 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 ZU$^0DDm++x6IAa3r%zfMl^b}{{~yI-I<No$ literal 0 HcmV?d00001 diff --git a/src/ui/main.qml b/src/ui/main.qml index bd78266..0093041 100644 --- a/src/ui/main.qml +++ b/src/ui/main.qml @@ -103,7 +103,6 @@ Rectangle { Layout.fillWidth: true } - ExamplesPage { id: examplesPage Layout.fillWidth: true @@ -114,8 +113,8 @@ Rectangle { Layout.fillWidth: true } - Network { - id: networkPage + DSManagement { + id: dsManagementPage Layout.fillWidth: true } @@ -133,12 +132,11 @@ Rectangle { states: [ State { name: "vertical" - when: root.height >= 400 - + when: root.height >= 600 }, State { name: "horizontal" - when: root.height < 400 + when: root.height < 600 PropertyChanges { target: qdsicon1 @@ -164,15 +162,14 @@ Rectangle { id: drawer width: 150 height: root.height + ScrollView { id: scrollview anchors.fill: parent + padding: 0 ColumnLayout { id: column - anchors.fill: drawer - anchors.rightMargin: 10 - anchors.leftMargin: 10 width: Math.max(implicitWidth, drawer.availableWidth) height: Math.max(implicitHeight, drawer.availableHeight) @@ -214,8 +211,8 @@ Rectangle { } TabButton { - id: network - text: qsTr("Network") + id: dsManagement + text: qsTr("Design Studio") Layout.fillWidth: true checkable: true autoExclusive: true @@ -265,4 +262,110 @@ Rectangle { } } } + + Popup { + property bool popupCloseReceived : false + id: popup + anchors.centerIn: parent + width: 300 + height: 100 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape + + onClosed: { + if (!popupCloseReceived) { + backend.popupInterrupted(); + } + } + + ColumnLayout { + anchors.fill: parent + Text { + id: popupText + } + Item { + id: name + } + ProgressBar { + id: popupProgressBar + Layout.fillWidth: true + to: 100 + } + } + + Connections { + target: backend + function onPopupOpen() { + popup.open() + popup.popupCloseReceived = false + } + function onPopupClose() { + popup.popupCloseReceived = true + popup.close() + } + function onPopupTextChanged(text) { + popupText.text = text + } + function onPopupProgressIndeterminateChanged(status) { + popupProgressBar.indeterminate = status + } + function onDownloadProgress(progress) { + popupProgressBar.value = progress + } + } + } + + Popup { + property string deviceId: "" + id: pinPopup + anchors.centerIn: parent + width: 250 + height: 250 + visible: false + modal: true + focus: true + contentItem: Rectangle { + color: "lightgrey" + border.color: "black" + border.width: 2 + radius: 10 + Text { + text: "Enter 4 digit pin\nthat you see on Design Studio" + anchors.centerIn: parent + horizontalAlignment: Qt.AlignHCenter + } + TextField { + id: pinField + anchors.top: parent.top + anchors.topMargin: 20 + anchors.horizontalCenter: parent.horizontalCenter + width: 100 + height: 50 + font.pixelSize: 20 + inputMethodHints: Qt.ImhDigitsOnly + validator: IntValidator { bottom: 0; top: 9999 } + } + + Button { + text: "Send PIN" + anchors.bottom: parent.bottom + anchors.bottomMargin: 20 + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + backend.enterPin(pinPopup.deviceId, pinField.text); + pinPopup.close(); + } + } + } + + Connections { + target: backend + function onPinRequested(deviceId) { + console.log("Pin requested for device: " + deviceId) + pinPopup.visible = true + pinPopup.deviceId = deviceId + } + } + } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e290b17..d76888c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,18 +1,23 @@ set(TARGET_NAME ${PROJECT_NAME}_test) -find_package(Qt6 REQUIRED COMPONENTS Test Core Quick Gui Multimedia) +find_package(Qt6 REQUIRED COMPONENTS Test Core Quick Gui Multimedia WebSockets) enable_testing(true) qt_add_executable(${TARGET_NAME} - tst_qtuiviewer.cpp - tst_qtuiviewer.h + tst_projectmanager.cpp tst_projectmanager.h + tst_serviceconnector.cpp tst_serviceconnector.h + tst_designstudio.cpp tst_designstudio.h + tst_dsdiscovery.cpp tst_dsdiscovery.h + tst_dsmanager.cpp tst_dsmanager.h + mock.h + main.cpp ) add_test(NAME ${TARGET_NAME} COMMAND qtuiviewer_test) target_link_libraries( ${TARGET_NAME} PRIVATE - Qt::Test Qt::Core Qt::Quick Qt::Gui Qt::Multimedia + Qt::Test Qt::Core Qt::Quick Qt::Gui Qt::Multimedia Qt::WebSockets qtuiviewerlib ) diff --git a/tests/main.cpp b/tests/main.cpp new file mode 100644 index 0000000..e991d91 --- /dev/null +++ b/tests/main.cpp @@ -0,0 +1,103 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include <QGuiApplication> +#include <QTemporaryFile> +#include <QTest> + +#include "tst_designstudio.h" +#include "tst_dsdiscovery.h" +#include "tst_projectmanager.h" +#include "tst_serviceconnector.h" + +#define DEBUG_LINE qDebug() << "Debugline:" << __LINE__ + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QStringList initialArgs = app.arguments(); + DEBUG_LINE << "Arguments:" << initialArgs; + + QString outputFileName; + QString appName = initialArgs.first(); + initialArgs.removeFirst(); + + if (initialArgs.contains("-o")) { + int index = initialArgs.indexOf("-o"); + if (index < initialArgs.size() - 1) { + outputFileName = initialArgs.at(index + 1); + outputFileName.remove(",junitxml"); // remove the junitxml extension if it's provided + DEBUG_LINE << "Find output file name inside the args:" << outputFileName; + + initialArgs.removeAt(index); // remove the -o flag + initialArgs.removeAt(index); // remove the output file name + } + } + + if (outputFileName.isEmpty()) { + qDebug() << "Output file name not provided. Using the default name: output.junitxml"; + outputFileName = "output.junitxml"; + } + + QFile outputFile{outputFileName}; + if (!outputFile.open(QIODevice::Append)) { + DEBUG_LINE << "Failed to open the output file"; + return 1; + } + + int status = 0; + auto runTest = [&status, &outputFile, initialArgs, appName](QObject *obj) { + // temporary file to store the individual test results. + // we'll append the results to the output file. + QTemporaryFile file; + file.open(); + DEBUG_LINE << "Temporary file:" << file.fileName(); + + QStringList args{"-o", QString(file.fileName()).append(",junitxml")}; + args.prepend(appName); + args.append(initialArgs); + + DEBUG_LINE << "Running test" << obj->metaObject()->className() << "with arguments:" << args; + status |= QTest::qExec(obj, args); + + // append the test results to the output file + file.readLine(); // skip the first line because it's the xml header + outputFile.write(file.readAll()); + }; + + outputFile.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); + outputFile.write("<testsuites>\n"); + + runTest(new TestServiceConnector); + runTest(new TestProjectManager); + runTest(new TestDesignStudio); + runTest(new TestDesignStudioDiscovery); + + outputFile.write("</testsuites>\n"); + + DEBUG_LINE << "Test suite finished with status:" << status; + return status; +} diff --git a/tests/mock.h b/tests/mock.h new file mode 100644 index 0000000..f7eb895 --- /dev/null +++ b/tests/mock.h @@ -0,0 +1,99 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QJsonDocument> +#include <QJsonObject> +#include <QObject> +#include <QTimer> +#include <QUdpSocket> +#include <QWebSocket> +#include <QWebSocketServer> +#include <qtypes.h> + +#include "backend/dsconnector/tcpdatatypes.h" + +#define UDP_PORT 53452 + +class DesignStudioMock : public QObject +{ + Q_OBJECT + +private: + // self advertisement over udp + QTimer m_discoveryTimer; + const QString m_id; + + // websocket connection to design studio + QWebSocketServer m_webSocketServer; + +public: + DesignStudioMock(QObject *parent = nullptr, const QString &id = "123456") + : QObject(parent) + , m_id(id) + , m_webSocketServer("DesignStudioMock", QWebSocketServer::NonSecureMode, this) + + { + connect(&m_discoveryTimer, &QTimer::timeout, [this] { + QUdpSocket udpSocket; + + QJsonObject message; + message["name"] = "__designstudio__"; + message["id"] = m_id; + QByteArray datagram = QJsonDocument(message).toJson(QJsonDocument::Compact); + + udpSocket.writeDatagram(datagram, QHostAddress::LocalHost, UDP_PORT); + udpSocket.writeDatagram(datagram, QHostAddress::Broadcast, UDP_PORT); + }); + + m_discoveryTimer.setInterval(100); + m_discoveryTimer.start(); + + connect(&m_webSocketServer, &QWebSocketServer::newConnection, [this] { + QWebSocket *socket = m_webSocketServer.nextPendingConnection(); + connect(socket, + &QWebSocket::textMessageReceived, + [this, socket](const QString &message) { + QJsonObject json = QJsonDocument::fromJson(message.toUtf8()).object(); + }); + }); + } + + void enableDiscovery(const bool enable) + { + if (enable) { + m_discoveryTimer.start(); + } else { + m_discoveryTimer.stop(); + } + } + + void connectToDesignStudio(); + void disconnectFromDesignStudio(); + +signals: + void disconnected(); +}; diff --git a/tests/scripts/get_test_result.sh b/tests/scripts/get_test_result.sh new file mode 100644 index 0000000..c4758af --- /dev/null +++ b/tests/scripts/get_test_result.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# first argument is the path to the output.junit.xml file +if [ $# -ne 1 ]; then + echo "Usage: $0 <path_to_output.junit.xml>" + exit 1 +fi + +UNIT_TEST_RESULT_FILE=$1 + +# check if the file exists +if [ ! -f "${UNIT_TEST_RESULT_FILE}" ]; then + echo "File ${UNIT_TEST_RESULT_FILE} not found!" + exit 1 +fi + +# check if the file is empty +if [ ! -s "${UNIT_TEST_RESULT_FILE}" ]; then + echo "File ${UNIT_TEST_RESULT_FILE} is empty!" + exit 1 +fi + +RETVAL=$(grep -c "<failure" ${UNIT_TEST_RESULT_FILE}) + +if [ ${RETVAL} -ne 0 ]; then + echo "Test failed!" + exit 1 +fi diff --git a/tests/tst_designstudio.cpp b/tests/tst_designstudio.cpp new file mode 100644 index 0000000..b22b4e6 --- /dev/null +++ b/tests/tst_designstudio.cpp @@ -0,0 +1,301 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "tst_designstudio.h" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QLatin1String> +#include <QSignalSpy> +#include <QTest> +#include <QWebSocket> +#include <QWebSocketServer> + +#include "backend/dsconnector/ds.h" +#include "backend/dsconnector/tcpdatatypes.h" + +QJsonObject createPackage(const QLatin1String &type, const QString &data) +{ + QJsonObject obj; + obj.insert("dataType", type.toString()); + obj.insert("data", data); + return obj; +} + +void TestDesignStudio::initTestCase() +{ + qDebug() << "Initialize TestDesignStudio"; + ds = new DesignStudio("localhost", 40000, "id", "deviceId"); + QVERIFY(ds != nullptr); +} + +void TestDesignStudio::testIpv4Addr() +{ + QCOMPARE(ds->ipv4Addr(), QString("localhost")); +} + +void TestDesignStudio::testId() +{ + QCOMPARE(ds->designStudioId(), QString("id")); +} + +void TestDesignStudio::testPort() +{ + QCOMPARE(ds->port(), quint16(40000)); +} + +void TestDesignStudio::testUrl() +{ + QCOMPARE(ds->url(), QUrl("ws://localhost:40000")); +} + +void TestDesignStudio::testDeviceId() +{ + QCOMPARE(ds->deviceId(), QString("deviceId")); +} + +void TestDesignStudio::testConnected() +{ + QHostAddress addr = QHostAddress::LocalHost; + QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); + QVERIFY(server.listen(addr, 40001)); + + DesignStudio ds(addr.toString(), 40001, "id", "deviceId"); + QTRY_VERIFY(ds.connected()); +} + +void TestDesignStudio::testSendRegistrationPin() +{ + QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); + QVERIFY(server.listen(QHostAddress::LocalHost, 40002)); + + DesignStudio ds("localhost", 40002, "id", "deviceId"); + QTRY_VERIFY(ds.connected()); + + QWebSocket *socket = server.nextPendingConnection(); + QVERIFY(socket != nullptr); + + connect(socket, &QWebSocket::textMessageReceived, [](const QString &message) { + qDebug() << "Received message" << message; + + QJsonObject obj = QJsonDocument::fromJson(message.toUtf8()).object(); + QVERIFY(obj.contains("dataType")); + QVERIFY(obj.contains("data")); + QVERIFY(obj.value("dataType").toString() == PackageToDesignStudio::registrationPin); + QVERIFY(obj.value("data").toString() == "1234"); + }); + + QSignalSpy spy(socket, &QWebSocket::textMessageReceived); + QVERIFY(spy.isValid()); + + ds.sendRegistrationPin("1234"); + QTRY_COMPARE(spy.count(), 1); + QTest::qWait(1000); +} + +void TestDesignStudio::testSendDeviceInfo() +{ + QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); + QVERIFY(server.listen(QHostAddress::LocalHost, 40003)); + + DesignStudio ds("localhost", 40003, "id", "deviceId"); + QTRY_VERIFY(ds.connected()); + + QWebSocket *socket = server.nextPendingConnection(); + QVERIFY(socket != nullptr); + + connect(socket, &QWebSocket::textMessageReceived, [](const QString &message) { + qDebug() << "Received message" << message; + + QJsonObject obj = QJsonDocument::fromJson(message.toUtf8()).object(); + QVERIFY(obj.contains("dataType")); + QVERIFY(obj.contains("data")); + QVERIFY(obj.value("dataType").toString() == PackageToDesignStudio::deviceInfo); + QVERIFY(obj.value("data").toObject().contains("screenHeight")); + QVERIFY(obj.value("data").toObject().contains("screenWidth")); + QVERIFY(obj.value("data").toObject().contains("os")); + QVERIFY(obj.value("data").toObject().contains("osVersion")); + QVERIFY(obj.value("data").toObject().contains("architecture")); + QVERIFY(obj.value("data").toObject().contains("deviceId")); + QVERIFY(obj.value("data").toObject().contains("appVersion")); + }); + + QSignalSpy spy(socket, &QWebSocket::textMessageReceived); + QVERIFY(spy.isValid()); + + const QJsonObject dsReadyPackage = createPackage(PackageFromDesignStudio::designStudioReady, + "data"); + socket->sendTextMessage(QJsonDocument(dsReadyPackage).toJson(QJsonDocument::Compact)); + + QTRY_COMPARE(spy.count(), 1); + QTest::qWait(500); +} + +void TestDesignStudio::testRegistrationPinRequestedSignal() +{ + QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); + QVERIFY(server.listen(QHostAddress::LocalHost, 40005)); + + DesignStudio ds("localhost", 40005, "id", "deviceId"); + QTRY_VERIFY(ds.connected()); + + QWebSocket *socket = server.nextPendingConnection(); + QVERIFY(socket != nullptr); + + QSignalSpy spy(&ds, &DesignStudio::registrationPinRequested); + QVERIFY(spy.isValid()); + + const QJsonObject pinRequestedPackage + = createPackage(PackageFromDesignStudio::pinVerificationRequested, "data"); + socket->sendTextMessage(QJsonDocument(pinRequestedPackage).toJson(QJsonDocument::Compact)); + + QTRY_COMPARE(spy.count(), 1); +} + +void TestDesignStudio::testPinVerificationFailedSignal() +{ + QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); + QVERIFY(server.listen(QHostAddress::LocalHost, 40006)); + + DesignStudio ds("localhost", 40006, "id", "deviceId"); + QTRY_VERIFY(ds.connected()); + + QWebSocket *socket = server.nextPendingConnection(); + QVERIFY(socket != nullptr); + + QSignalSpy spy(&ds, &DesignStudio::pinVerificationFailed); + QVERIFY(spy.isValid()); + + const QJsonObject pinVerificationFailedPackage + = createPackage(PackageFromDesignStudio::pinVerificationFailed, "data"); + socket->sendTextMessage( + QJsonDocument(pinVerificationFailedPackage).toJson(QJsonDocument::Compact)); + + QTRY_COMPARE(spy.count(), 1); +} + +void TestDesignStudio::testRegistrationSuccessfulSignal() +{ + QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); + QVERIFY(server.listen(QHostAddress::LocalHost, 40007)); + + DesignStudio ds("localhost", 40007, "id"); + QTRY_VERIFY(ds.connected()); + + QWebSocket *socket = server.nextPendingConnection(); + QVERIFY(socket != nullptr); + + QSignalSpy spy(&ds, &DesignStudio::registrationSucceeded); + QVERIFY(spy.isValid()); + + const QJsonObject deviceIdReceivedPackage + = createPackage(PackageFromDesignStudio::deviceIdReceived, "data"); + socket->sendTextMessage(QJsonDocument(deviceIdReceivedPackage).toJson(QJsonDocument::Compact)); + + QTRY_COMPARE(spy.count(), 1); +} + +void TestDesignStudio::testUnregisteredSignal() +{ + QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); + QVERIFY(server.listen(QHostAddress::LocalHost, 40009)); + + DesignStudio ds("localhost", 40009, "id", "deviceId"); + QTRY_VERIFY(ds.connected()); + + QWebSocket *socket = server.nextPendingConnection(); + QVERIFY(socket != nullptr); + + QSignalSpy spy(&ds, &DesignStudio::unregistered); + QVERIFY(spy.isValid()); + + const QJsonObject deviceIdDeclinedPackage + = createPackage(PackageFromDesignStudio::deviceIdDeclined, "data"); + socket->sendTextMessage(QJsonDocument(deviceIdDeclinedPackage).toJson(QJsonDocument::Compact)); + + QTRY_COMPARE(spy.count(), 1); +} + +void TestDesignStudio::testConnectionAliveSignal() +{ + QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); + QVERIFY(server.listen(QHostAddress::LocalHost, 40010)); + + DesignStudio ds("localhost", 40010, "id", "deviceId"); + QTRY_VERIFY(ds.connected()); + + QWebSocket *socket = server.nextPendingConnection(); + QVERIFY(socket != nullptr); + + QSignalSpy spy(&ds, &DesignStudio::dsOnline); + QVERIFY(spy.isValid()); + + const QJsonObject dsReadyPackage = createPackage(PackageFromDesignStudio::deviceIdAccepted, + "data"); + socket->sendTextMessage(QJsonDocument(dsReadyPackage).toJson(QJsonDocument::Compact)); + + QTRY_COMPARE(spy.count(), 1); +} + +void TestDesignStudio::testProjectIncomingSignal() +{ + QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); + QVERIFY(server.listen(QHostAddress::LocalHost, 40011)); + + DesignStudio ds("localhost", 40011, "id", "deviceId"); + QTRY_VERIFY(ds.connected()); + + QWebSocket *socket = server.nextPendingConnection(); + QVERIFY(socket != nullptr); + + QSignalSpy spy(&ds, &DesignStudio::projectIncoming); + QVERIFY(spy.isValid()); + + const QJsonObject projectIncomingPackage = createPackage(PackageFromDesignStudio::projectData, + "data"); + socket->sendTextMessage(QJsonDocument(projectIncomingPackage).toJson(QJsonDocument::Compact)); + + QTRY_COMPARE(spy.count(), 1); +} + +void TestDesignStudio::testProjectReceivedSignal() +{ + QWebSocketServer server("TestDesignStudio", QWebSocketServer::NonSecureMode); + QVERIFY(server.listen(QHostAddress::LocalHost, 40012)); + + DesignStudio ds("localhost", 40012, "id", "deviceId"); + QTRY_VERIFY(ds.connected()); + + QWebSocket *socket = server.nextPendingConnection(); + QVERIFY(socket != nullptr); + + QSignalSpy spy(&ds, &DesignStudio::projectReceived); + QVERIFY(spy.isValid()); + + const QString arbitraryData = "some arbitrary data"; + socket->sendBinaryMessage(arbitraryData.toUtf8()); + + QTRY_COMPARE(spy.count(), 1); +} diff --git a/tests/tst_designstudio.h b/tests/tst_designstudio.h new file mode 100644 index 0000000..518d12e --- /dev/null +++ b/tests/tst_designstudio.h @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "backend/dsconnector/ds.h" + +class TestDesignStudio : public QObject +{ + Q_OBJECT +private: + DesignStudio *ds; + +private slots: + // init + void initTestCase(); + + // test getters + void testIpv4Addr(); + void testId(); + void testPort(); + void testUrl(); + void testDeviceId(); + + // test status check + void testConnected(); + + // test send data + void testSendRegistrationPin(); + void testSendDeviceInfo(); + + // test signals + void testRegistrationPinRequestedSignal(); + void testPinVerificationFailedSignal(); + void testRegistrationSuccessfulSignal(); + void testUnregisteredSignal(); + void testConnectionAliveSignal(); + void testProjectIncomingSignal(); + void testProjectReceivedSignal(); +}; diff --git a/tests/tst_dsdiscovery.cpp b/tests/tst_dsdiscovery.cpp new file mode 100644 index 0000000..e5d1a7a --- /dev/null +++ b/tests/tst_dsdiscovery.cpp @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "tst_dsdiscovery.h" + +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> +#include <QSignalSpy> +#include <QUdpSocket> + +#include "backend/dsconnector/dsdiscovery.h" + +#define UDP_PORT 53452 + +void TestDesignStudioDiscovery::initTestCase() +{ + qDebug() << "Initialize TestDesignStudioDiscovery"; + + connect(&m_timer, &QTimer::timeout, [] { + QUdpSocket udpSocket; + + QJsonObject message; + message["name"] = "__designstudio__"; + message["id"] = "123456"; + QByteArray datagram = QJsonDocument(message).toJson(QJsonDocument::Compact); + + udpSocket.writeDatagram(datagram, QHostAddress::LocalHost, UDP_PORT); + udpSocket.writeDatagram(datagram, QHostAddress::Broadcast, UDP_PORT); + }); +} + +void TestDesignStudioDiscovery::testDesignStudioFoundSignal() +{ + qDebug() << "Test Design Studio Found Signal"; + + DesignStudioDiscovery dsDiscovery; + connect(&dsDiscovery, &DesignStudioDiscovery::designStudioFound, [](const QString &ipv4Addr) { + qDebug() << "Design Studio found at" << ipv4Addr; + }); + + m_timer.start(1000); + + QSignalSpy spy(&dsDiscovery, &DesignStudioDiscovery::designStudioFound); +} diff --git a/tests/tst_dsdiscovery.h b/tests/tst_dsdiscovery.h new file mode 100644 index 0000000..d8f092a --- /dev/null +++ b/tests/tst_dsdiscovery.h @@ -0,0 +1,41 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QTimer> + +class TestDesignStudioDiscovery : public QObject +{ + Q_OBJECT +private: + QTimer m_timer; + +private slots: + void initTestCase(); + + // test signals + void testDesignStudioFoundSignal(); +}; diff --git a/tests/tst_dsmanager.cpp b/tests/tst_dsmanager.cpp new file mode 100644 index 0000000..51f9a0c --- /dev/null +++ b/tests/tst_dsmanager.cpp @@ -0,0 +1,59 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "tst_dsmanager.h" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QTest> +#include <QUdpSocket> + +#include "backend/dsconnector/dsmanager.h" +#include "mock.h" + +void TestDesignStudioManager::testEnterPin() +{ + // Test enterPin() function +} + +void TestDesignStudioManager::testDesignStudioRegisteredSignal() +{ + // Test designStudioRegisteredSignal() function +} + +void TestDesignStudioManager::testDesignStudioUnregisteredSignal() +{ + // Test designStudioUnregisteredSignal() function +} + +void TestDesignStudioManager::testPinRequestedSignal() +{ + // Test pinRequestedSignal() function +} + +void TestDesignStudioManager::testProjectReceivedSignal() +{ + // Test projectReceivedSignal() function +} diff --git a/tests/tst_dsmanager.h b/tests/tst_dsmanager.h new file mode 100644 index 0000000..07442f2 --- /dev/null +++ b/tests/tst_dsmanager.h @@ -0,0 +1,44 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QObject> + +class TestDesignStudioManager : public QObject +{ + Q_OBJECT +private: +private slots: + + // test setters + void testEnterPin(); + + // test signals + void testDesignStudioRegisteredSignal(); + void testDesignStudioUnregisteredSignal(); + void testPinRequestedSignal(); + void testProjectReceivedSignal(); +}; diff --git a/tests/tst_qtuiviewer.cpp b/tests/tst_projectmanager.cpp similarity index 53% rename from tests/tst_qtuiviewer.cpp rename to tests/tst_projectmanager.cpp index 08499d8..c2caf9a 100644 --- a/tests/tst_qtuiviewer.cpp +++ b/tests/tst_projectmanager.cpp @@ -23,28 +23,28 @@ ** ****************************************************************************/ -#include "tst_qtuiviewer.h" +#include "tst_projectmanager.h" -#include <QSignalSpy> +#include <QJsonArray> +#include <QTest> -#include "backend/dsconnector.h" #include "backend/projectmanager.h" #include "backend/serviceconnector.h" #define DEMO_PROJECT_NAME "ClusterTutorial" -void TestQtUiViewer::initTestCase() +void TestProjectManager::initTestCase() { - qDebug() << "initTestCase"; + qDebug() << "Initialize TestProjectManager"; ServiceConnector sc; - m_demoProjectList = sc.fetchDemoList(); - QVERIFY(m_demoProjectList.has_value()); + std::optional<QJsonArray> demoProjectList = sc.fetchDemoList(); + QVERIFY(demoProjectList.has_value()); m_demoProjectData = sc.fetchDemo(DEMO_PROJECT_NAME); QVERIFY(m_demoProjectData.has_value()); QJsonObject projectInfo; - for (auto project : m_demoProjectList.value()) { + for (auto project : demoProjectList.value()) { if (DEMO_PROJECT_NAME == project.toObject().value("name").toString().remove(".qmlrc")) { m_demoInfoCorrect = project.toObject(); break; @@ -73,39 +73,17 @@ void TestQtUiViewer::initTestCase() {"ttlDays", 31}, {"uploadTime", "2023-10-27T13:58:22"}, {"userHash", "12038740912873462987"}}}; - qDebug() << "initTestCase done"; + qDebug() << "TestProjectManager initialized"; } -void TestQtUiViewer::fetchDemoProjectList() -{ - QStringList projectNames{"ClusterTutorial.qmlrc", - "CoffeeMachine.qmlrc", - "EBikeDesign.qmlrc", - "MaterialBundle.qmlrc", - "SideMenu.qmlrc", - "WebinarDemo.qmlrc"}; - - QStringList projectNamesFromJson; - for (const auto &project : m_demoProjectList.value()) { - projectNamesFromJson.append(project.toObject().value("name").toString()); - } - - QCOMPARE(projectNamesFromJson, projectNames); -} - -void TestQtUiViewer::fetchDemoProject() -{ - QCOMPARE(m_demoProjectData.has_value(), true); -} - -void TestQtUiViewer::cacheDemoProject() +void TestProjectManager::cacheDemoProject() { ProjectManager pm; QCOMPARE(pm.cacheDemoProject(m_demoProjectData.value(), m_demoInfoCorrect), true); } -void TestQtUiViewer::isDemoProjectCachedTrue() +void TestProjectManager::isDemoProjectCachedTrue() { ProjectManager pm; @@ -113,7 +91,7 @@ void TestQtUiViewer::isDemoProjectCachedTrue() QCOMPARE(pm.isDemoProjectCached(m_demoInfoCorrect), true); } -void TestQtUiViewer::isDemoProjectCachedFalse() +void TestProjectManager::isDemoProjectCachedFalse() { ProjectManager pm; @@ -121,7 +99,7 @@ void TestQtUiViewer::isDemoProjectCachedFalse() QCOMPARE(pm.isDemoProjectCached(m_demoInfoIncorrect), false); } -void TestQtUiViewer::clearDemoProjectCache() +void TestProjectManager::clearDemoProjectCache() { ProjectManager pm; QVERIFY(pm.cacheDemoProject(m_demoProjectData.value(), m_demoInfoCorrect)); @@ -130,27 +108,27 @@ void TestQtUiViewer::clearDemoProjectCache() QCOMPARE(pm.isDemoProjectCached(m_demoInfoCorrect), false); } -void TestQtUiViewer::cacheUserProject() +void TestProjectManager::cacheUserProject() { ProjectManager pm; QCOMPARE(pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect), true); } -void TestQtUiViewer::isUserProjectCachedTrue() +void TestProjectManager::isUserProjectCachedTrue() { ProjectManager pm; pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect); QCOMPARE(pm.isProjectCached(m_userProjectInfoCorrect), true); } -void TestQtUiViewer::isUserProjectCachedFalse() +void TestProjectManager::isUserProjectCachedFalse() { ProjectManager pm; QVERIFY(pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect)); QCOMPARE(pm.isProjectCached(m_userProjectInfoIncorrect), false); } -void TestQtUiViewer::clearUserProjectCache() +void TestProjectManager::clearUserProjectCache() { ProjectManager pm; QVERIFY(pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect)); @@ -160,113 +138,10 @@ void TestQtUiViewer::clearUserProjectCache() QCOMPARE(pm.isProjectCached(m_userProjectInfoCorrect), false); } -void TestQtUiViewer::clearNonExistentUserProjectCache() +void TestProjectManager::clearNonExistentUserProjectCache() { ProjectManager pm; QVERIFY(!pm.isProjectCached(m_userProjectInfoIncorrect)); pm.clearCachedProject(m_userProjectInfoIncorrect); QCOMPARE(pm.isProjectCached(m_userProjectInfoIncorrect), false); } - -void TestQtUiViewer::initDsConnector() -{ - DesignStudioConnector dsc(40000); - - QTcpSocket socket; - socket.connectToHost("localhost", 40000); - - QVERIFY(socket.waitForConnected(1000)); -} - -void TestQtUiViewer::incomingProjectTrue() -{ - DesignStudioConnector dsc(40001); - - QTcpSocket socket; - socket.connectToHost("localhost", 40001); - - QVERIFY(socket.waitForConnected(1000)); - - // we'll write a sample project over the tcp connection - // and expect the DesignStudioConnector to emit the projectIncoming signal - QSignalSpy spy(&dsc, &DesignStudioConnector::projectIncoming); - QVERIFY(spy.isValid()); - - QByteArray projectData = "qres::qmlrc-start::"; - socket.write(projectData); - QVERIFY(socket.waitForBytesWritten(1000)); - - QTRY_COMPARE(spy.count(), 1); -} - -void TestQtUiViewer::incomingProjectFalse() -{ - DesignStudioConnector dsc(40002); - - QTcpSocket socket; - socket.connectToHost("localhost", 40002); - - QVERIFY(socket.waitForConnected(1000)); - - // we'll write a arbitrary data over the tcp connection - // and don't expect the DesignStudioConnector to emit the projectIncoming signal - QSignalSpy spy(&dsc, SIGNAL(projectIncoming())); - - QByteArray projectData = "some arbitrary data"; - socket.write(projectData); - QVERIFY(socket.waitForBytesWritten(1000)); - - QTRY_COMPARE(spy.count(), 0); -} - -void TestQtUiViewer::receiveProject() -{ - DesignStudioConnector dsc(40003); - - QTcpSocket socket; - socket.connectToHost("localhost", 40003); - - QVERIFY(socket.waitForConnected(1000)); - - // we'll write a sample project over the tcp connection - // and expect the DesignStudioConnector to emit the projectReceived signal - QSignalSpy spy(&dsc, &DesignStudioConnector::projectReceived); - QVERIFY(spy.isValid()); - - QByteArray projectData = "qres::qmlrc-start::"; - socket.write(projectData); - QVERIFY(socket.waitForBytesWritten(1000)); - - spy.wait(1000); - - projectData = "Hello World::qmlrc-end::"; - socket.write(projectData); - QVERIFY(socket.waitForBytesWritten(1000)); - - QTRY_COMPARE(spy.count(), 1); -} - -void TestQtUiViewer::networkStatusUpdated() -{ - DesignStudioConnector dsc(40004); - - QTcpSocket socket; - - QSignalSpy spy(&dsc, &DesignStudioConnector::networkStatusUpdated); - QVERIFY(spy.isValid()); - - socket.connectToHost("localhost", 40004); - QVERIFY(socket.waitForConnected(1000)); - - spy.wait(500); - QTRY_COMPARE(spy.count(), 1); - - socket.disconnectFromHost(); - socket.waitForDisconnected(1000); - QVERIFY(socket.state() == QAbstractSocket::UnconnectedState); - - spy.wait(500); - QTRY_COMPARE(spy.count(), 2); -} - -QTEST_MAIN(TestQtUiViewer); diff --git a/tests/tst_qtuiviewer.h b/tests/tst_projectmanager.h similarity index 82% rename from tests/tst_qtuiviewer.h rename to tests/tst_projectmanager.h index c158d37..4e7635a 100644 --- a/tests/tst_qtuiviewer.h +++ b/tests/tst_projectmanager.h @@ -23,15 +23,14 @@ ** ****************************************************************************/ -#include <QJsonArray> +#pragma once + #include <QJsonObject> -#include <QtTest/QTest> -class TestQtUiViewer : public QObject +class TestProjectManager : public QObject { Q_OBJECT private: - std::optional<QJsonArray> m_demoProjectList; std::optional<QByteArray> m_demoProjectData; QJsonObject m_demoInfoCorrect; QJsonObject m_demoInfoIncorrect; @@ -39,12 +38,9 @@ private: QJsonObject m_userProjectInfoIncorrect; private slots: + // init void initTestCase(); - // service connector tests - void fetchDemoProjectList(); - void fetchDemoProject(); - // project manager tests - demo project void cacheDemoProject(); void isDemoProjectCachedTrue(); @@ -57,11 +53,4 @@ private slots: void isUserProjectCachedFalse(); void clearUserProjectCache(); void clearNonExistentUserProjectCache(); - - // ds connector tests - void initDsConnector(); - void incomingProjectTrue(); - void incomingProjectFalse(); - void receiveProject(); - void networkStatusUpdated(); }; diff --git a/tests/tst_serviceconnector.cpp b/tests/tst_serviceconnector.cpp new file mode 100644 index 0000000..9b6c66e --- /dev/null +++ b/tests/tst_serviceconnector.cpp @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "tst_serviceconnector.h" + +#include <QJsonObject> +#include <QTest> + +#include "backend/serviceconnector.h" + +#define DEMO_PROJECT_NAME "ClusterTutorial" + +void TestServiceConnector::initTestCase() +{ + qDebug() << "Initialize TestServiceConnector"; + ServiceConnector sc; + m_demoProjectList = sc.fetchDemoList(); + QVERIFY(m_demoProjectList.has_value()); + + m_demoProjectData = sc.fetchDemo(DEMO_PROJECT_NAME); + QVERIFY(m_demoProjectData.has_value()); + + qDebug() << "TestServiceConnector initialized"; +} + +void TestServiceConnector::fetchDemoProjectList() +{ + QStringList projectNames{"ClusterTutorial.qmlrc", + "CoffeeMachine.qmlrc", + "EBikeDesign.qmlrc", + "MaterialBundle.qmlrc", + "SideMenu.qmlrc", + "WebinarDemo.qmlrc"}; + + QStringList projectNamesFromJson; + for (const auto &project : m_demoProjectList.value()) { + projectNamesFromJson.append(project.toObject().value("name").toString()); + } + + QCOMPARE(projectNamesFromJson, projectNames); +} + +void TestServiceConnector::fetchDemoProject() +{ + QCOMPARE(m_demoProjectData.has_value(), true); +} diff --git a/tests/tst_serviceconnector.h b/tests/tst_serviceconnector.h new file mode 100644 index 0000000..b0dfa99 --- /dev/null +++ b/tests/tst_serviceconnector.h @@ -0,0 +1,43 @@ +/**************************************************************************** +** +** Copyright (C) 2024 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QJsonArray> + +class TestServiceConnector : public QObject +{ + Q_OBJECT +private: + std::optional<QJsonArray> m_demoProjectList; + std::optional<QByteArray> m_demoProjectData; + +private slots: + void initTestCase(); + + // service connector tests + void fetchDemoProjectList(); + void fetchDemoProject(); +}; -- GitLab