Skip to content
Snippets Groups Projects
backend.cpp 16.56 KiB
/****************************************************************************
**
** 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 "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");

    // Initialize background update
    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
{
#ifdef QT_DEBUG
    const QString buildType = "Debug";
#else
    const QString buildType = "Release";
#endif
    return {QCoreApplication::applicationVersion() + "\nTechnology Preview - "
            + QString(CMAKE_VAR_GIT_VERSION) + "\nQt " + QString(QT_VERSION_STR) + " - " + buildType
            + " Build" + "\nQt Quick Components " + QString(CMAKE_VAR_QT_QUICK_COMPONENTS_VERSION)
            + "\nZXing-Cpp: " + QString(CMAKE_VAR_ZXING_VERSION)
            + "\nOpenSSL support: " + QVariant(QSslSocket::supportsSsl()).toString()};
}

void Backend::setAutoScaleProject(const bool &enabled)
{
    QSettings().setValue(Constants::Settings::ProjectManager::AutoScale, enabled);
    if (enabled) {
        qDebug() << "Auto scale project is enabled";
    } else {
        qDebug() << "Auto scale project is disabled";
    }
}

void Backend::setUserHash(const QString &userHash)
{
    QSettings().setValue(Constants::Settings::Backend::UserHash, userHash);
    emit userHashChanged();
}

bool Backend::autoScaleProject()
{
    return QSettings().value(Constants::Settings::ProjectManager::AutoScale, true).toBool();
}

QString Backend::userHash()
{
    return QSettings().value(Constants::Settings::Backend::UserHash).toString();
}

void Backend::updatePopup(const QString &text, bool indeterminate)
{
    emit popupTextChanged(text);
    emit popupProgressIndeterminateChanged(indeterminate);
    QEventLoop().processEvents(QEventLoop::AllEvents, 1000);
}

void Backend::initializeProjectManager()
{
    m_projectManager.reset(new ProjectManager);
    connect(m_projectManager.get(), &ProjectManager::closingProject, this, [&] {
        emit popupClose();
        m_projectManager.reset();
    });
}

void Backend::initDesignStudioManager()
{
    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_dsConnectorThread, &QThread::finished, this, [this] {
        qDebug() << "Design Studio Manager thread finished";
        m_designStudioManager.reset();
    });

    connect(&m_serviceConnector,
            &ServiceConnector::downloadProgress,
            this,
            &Backend::downloadProgress);
}

void Backend::runDsProject(const QByteArray &projectData)
{
    initializeProjectManager();
    emit popupOpen();
    updatePopup("Unpacking project...");
    QString projectPath = m_projectManager->unpackProject(projectData);
    updatePopup("Running project...");

    if (!m_projectManager->runProject(projectPath))
        qCritical() << "Could not run project. Please check the logs for more information.";
    else {
        m_projectManager->showAppWindow();
    }

    emit popupClose();
}

void Backend::runDemoProject(const QString &projectName)
{
    initializeProjectManager();
    qDebug() << "Checking if demo project is cached for " << projectName;
    emit popupOpen();

    // sample project info
    // [{"lastUpdate":1701947766739.9812,"name":"ClusterTutorial.qmlrc"}]
    const std::optional<QJsonArray> projectList = m_serviceConnector.fetchDemoList();

    if (projectList == std::nullopt) {
        qCritical()
            << "Could not fetch demo project list. Please check your internet connection and "
               "try again.";
        emit popupClose();
        return;
    }

    QJsonObject projectInfo;
    for (auto project : projectList.value()) {
        if (projectName == project.toObject().value("name").toString().remove(".qmlrc")) {
            projectInfo = project.toObject();
            break;
        }
    }

    const bool cached = m_projectManager->isDemoProjectCached(projectInfo);

    if (!cached) {
        updatePopup("Downloading demo project...", false);

        const std::optional<QByteArray> project = m_serviceConnector.fetchDemo(projectName);

        if (project == std::nullopt) {
            qCritical() << "Could not download demo project. Please check the logs for more "
                           "information.";
            emit popupClose();
            return;
        }

        updatePopup("Caching demo project...");
        if (!m_projectManager->cacheDemoProject(project.value(), projectInfo)) {
            qCritical()
                << "Could not cache demo project. Please check the logs for more information.";
            emit popupClose();
            return;
        }
    } else {
        qDebug() << "Demo project is cached. Running cached project...";
    }

    updatePopup("Running demo project...");
    if (!m_projectManager->runDemoProject(projectName))
        qCritical() << "Could not run demo project. Please check the logs for more information.";
    else {
        updatePopup("Showing the app window...");
        m_projectManager->showAppWindow();
    }

    emit popupClose();
}

void Backend::clearDemoCaches()
{
    emit popupOpen();
    updatePopup("Clearing demo caches...");
    ProjectManager().clearDemoCaches();
    emit popupClose();
}

void Backend::runUserProject(const QString &projectName, const QString &password)
{
    initializeProjectManager();
    updatePopup("Running user project");
    emit popupOpen();

    qDebug() << "Running user project:" << projectName;

    // fetch the project list to check if the project is cached
    const std::optional<QJsonArray> projectList = m_serviceConnector.fetchUserProjectList(
        userHash());

    if (projectList == std::nullopt) {
        qCritical()
            << "Could not fetch user project list. Please check your internet connection and "
               "try again.";
        emit popupClose();
        return;
    }

    QJsonObject projectInfo;
    for (const auto &project : projectList.value()) {
        if (projectName == project.toObject().value("appName").toString()) {
            projectInfo = project.toObject();
            break;
        }
    }

    const bool projectCached = m_projectManager->isProjectCached(projectInfo);

    if (!projectCached) {
        qDebug("Project is not cached. Downloading...");
        updatePopup("Project is not cached. Downloading...", false);
        const std::optional<QByteArray> projectData
            = m_serviceConnector.fetchUserProject(userHash(), projectName, password);

        if (projectData == std::nullopt) {
            qCritical()
                << "Could not download project. Please check the logs for more information.";
            emit popupClose();
            return;
        }

        updatePopup("Caching user project...");
        if (!m_projectManager->cacheProject(projectData.value(), projectInfo)) {
            qCritical() << "Could not cache project. Please check the logs for more information.";
            emit popupClose();
            return;
        }
    }

    qDebug("Project is cached. Running cached project...");
    updatePopup("Running cached project...");
    if (!m_projectManager->runCachedProject(projectInfo))
        qCritical() << "Could not run project. Please check the logs for more information.";
    else {
        updatePopup("Showing the app window...");
        m_projectManager->showAppWindow();
    }

    emit popupClose();
}

void Backend::runOnlineProject(const QString &url)
{
    initializeProjectManager();
    emit popupOpen();
    updatePopup("Downloading...", false);
    const std::optional<QByteArray> projectData = m_serviceConnector.fetchProject(url);

    if (projectData == std::nullopt) {
        qCritical() << "Could not download project. Please check the logs for more information.";
        emit popupClose();
        return;
    }

    updatePopup("Unpacking project...");
    QString projectPath = m_projectManager->unpackProject(projectData.value());
    updatePopup("Running project...");

    if (!m_projectManager->runProject(projectPath))
        qCritical() << "Could not run project. Please check the logs for more information.";
    else {
        m_projectManager->showAppWindow();
    }

    emit popupClose();
}

void Backend::updateUserProjectList()
{
    const QString userHash = Backend::userHash();

    if (userHash.isEmpty()) {
        return;
    }

    qDebug() << "Fetching available project list for user:" << userHash;
    const std::optional<QJsonArray> projectList = m_serviceConnector.fetchUserProjectList(userHash);

    if (projectList == std::nullopt) {
        qWarning(
            "Could not fetch project list. Please check your internet connection and try again.");
    } else if (projectList.value() == m_projectList) {
        qDebug("No new projects are available");
    } else {
        qDebug("List of available projects fetched:");
        for (const auto &project : projectList.value()) {
            const QString projectName{project.toObject().value("appName").toString()};
            qDebug() << "--" << projectName;
        }

        // check if any project is removed on the cloud
        for (const auto &project : m_projectList) {
            const QString projectName{project.toObject().value("appName").toString()};
            if (!projectList.value().contains(project)) {
                qDebug() << "Project removed:" << projectName << ". Removing from cache...";
                // remove the project from the cache
                ProjectManager().clearCachedProject(project.toObject());
            }
        }
    }

    // we need to set m_projectList even if it is empty
    // because this triggers the onModelChanged function in the QML
    // in order to update the UI
    m_projectList = projectList.value();
    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)
{
    // 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.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 format";
        qWarning() << "URL:" << url.toString();
    }
}

void Backend::popupInterrupted()
{
    qDebug() << "Popup closed prematurely. Interrupting active downloads (if any)";
    QMetaObject::invokeMethod(&m_serviceConnector, "interrupted", Qt::QueuedConnection);
}