Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • alcroito/qt-ui-viewer
  • design-studio/design-viewer/qt-ui-viewer
2 results
Show changes
Showing
with 805 additions and 544 deletions
<?xml version="1.0"?> <?xml version="1.0"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.qt.qtdesignviewer" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="auto" android:versionCode="26" android:versionName="1.2"> android:installLocation="auto" android:versionCode="@GOOGLE_PLAY_APP_VERSION@"
android:versionName="@CMAKE_VAR_GIT_VERSION@">
<!-- %%INSERT_PERMISSIONS --> <!-- %%INSERT_PERMISSIONS -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- %%INSERT_FEATURES --> <!-- %%INSERT_FEATURES -->
<supports-screens android:anyDensity="true" android:largeScreens="true" <supports-screens android:anyDensity="true" android:largeScreens="true"
android:normalScreens="true" android:smallScreens="true" /> android:normalScreens="true" android:smallScreens="true" />
<application android:name="org.qtproject.qt.android.bindings.QtApplication" <application android:icon="@mipmap/ic_launcher"
android:name="org.qtproject.qt.android.bindings.QtApplication"
android:extractNativeLibs="true" android:hardwareAccelerated="true" android:extractNativeLibs="true" android:hardwareAccelerated="true"
android:label="Qt UI Viewer" android:requestLegacyExternalStorage="true" android:label="Qt UI Viewer" android:requestLegacyExternalStorage="true"
android:allowNativeHeapPointerTagging="false" android:icon="@mipmap/app_icon"> android:allowNativeHeapPointerTagging="false">
<profileable android:shell="true" android:enabled="true" /> <profileable android:shell="true" android:enabled="true" />
<activity android:name="org.qtproject.qt.android.bindings.QtActivity" <activity android:name="org.qtproject.qt.android.bindings.QtActivity"
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
android:label="Qt UI Viewer" android:launchMode="singleTask" android:label="Qt UI Viewer" android:launchMode="singleTask"
android:screenOrientation="unspecified" android:exported="true"> android:screenOrientation="unspecified" android:exported="true"
android:windowSoftInputMode="adjustResize" android:keepScreenOn="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
...@@ -30,9 +34,11 @@ ...@@ -30,9 +34,11 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:host="designviewer.qt.io" android:scheme="https" /> <data android:host="designviewer.qt.io" android:scheme="https" />
</intent-filter> </intent-filter>
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/> <meta-data android:name="android.app.lib_name"
<meta-data android:name="android.app.arguments" android:value="-- %%INSERT_APP_ARGUMENTS%% --"/> android:value="-- %%INSERT_APP_LIB_NAME%% --" />
<meta-data android:name="android.app.extract_android_style" android:value="minimal"/> <meta-data android:name="android.app.arguments"
android:value="-- %%INSERT_APP_ARGUMENTS%% --" />
<meta-data android:name="android.app.extract_android_style" android:value="minimal" />
</activity> </activity>
</application> </application>
</manifest> </manifest>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.99"
android:scaleY="0.99"
android:translateX="0.54"
android:translateY="0.54">
<path
android:pathData="M0,13.5V108H94.5L108,94.5V0H13.5L0,13.5Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="104.99"
android:startY="-26.62"
android:endX="11.55"
android:endY="126.08"
android:type="linear">
<item android:offset="0.14" android:color="#FF014B57"/>
<item android:offset="1" android:color="#FF0F7080"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.66"
android:scaleY="0.66"
android:translateX="17.37"
android:translateY="17.04">
<group>
<clip-path
android:pathData="M0.25,0.13h107.75v107.75h-107.75z"/>
<path
android:pathData="M28.84,62.54C28.84,67.29 31.25,69.66 36.08,69.66C40.91,69.66 43.33,67.29 43.33,62.54V35.42H51.59V62.36C51.59,67.35 50.29,71.02 47.69,73.37C45.14,75.69 41.27,76.84 36.08,76.84C30.89,76.84 27,75.69 24.41,73.37C21.85,71.02 20.58,67.35 20.58,62.36V35.42H28.84V62.54ZM82.13,35.42H90.81L81.35,76.13H66.09L56.63,35.42H65.31L72.38,68.94H75.07L82.13,35.42Z"
android:fillColor="#2CDE85"/>
</group>
</group>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
\ No newline at end of file
src/android/res/mipmap-hdpi/ic_launcher.webp

2.04 KiB

src/android/res/mipmap-hdpi/ic_launcher_round.webp

3.5 KiB

src/android/res/mipmap-mdpi/ic_launcher.webp

1.4 KiB

src/android/res/mipmap-mdpi/ic_launcher_round.webp

2.29 KiB

src/android/res/mipmap-xhdpi/ic_launcher.webp

2.62 KiB

src/android/res/mipmap-xhdpi/ic_launcher_round.webp

4.84 KiB

src/android/res/mipmap-xxhdpi/ic_launcher.webp

3.79 KiB

src/android/res/mipmap-xxhdpi/ic_launcher_round.webp

7.01 KiB

src/android/res/mipmap-xxxhdpi/ic_launcher.webp

4.65 KiB

src/android/res/mipmap-xxxhdpi/ic_launcher_round.webp

9.29 KiB

<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<string name="app_name">Qt UI Viewer</string>
</resources>
/****************************************************************************
**
** 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 "qapplication.h"
#include <QCameraDevice>
#include <QDesktopServices>
#include <QElapsedTimer>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonObject>
#include <QMediaDevices>
#include <QMessageBox>
#include <QPermission>
#include <QSettings>
#include <QSslSocket>
#include <QVideoSink>
#include <QtConcurrent>
#include "../3rdparty/zxing-cpp/example/ZXingQtReader.h"
using namespace ZXingQt;
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("https", this, "parseDesignViewerUrl");
connect(&m_dsConnectorThread,
&QThread::started,
this,
&Backend::initDesignStudioConnector,
Qt::DirectConnection);
m_dsConnectorThread.start();
connect(&m_serviceConnector,
&ServiceConnector::downloadProgress,
this,
&Backend::downloadProgress);
}
void Backend::initialize()
{
const QRect screenGeometry = QGuiApplication::primaryScreen()->geometry();
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());
#ifdef QT_DEBUG
const QString buildType = "Debug";
#else
const QString buildType = "Release";
#endif
const QString buildInfo = QCoreApplication::applicationVersion() + "\nTechnology Preview - "
+ QString(GIT_VERSION) + "\nQt " + QString(QT_VERSION_STR) + " - "
+ buildType + " Build" + "\nQt Quick Components "
+ QString(QT_QUICK_COMPONENTS_VERSION) + "\nOpenSSL support: "
+ QVariant(QSslSocket::supportsSsl()).toString();
emit buildInfoChanged(buildInfo);
if (userHash().isEmpty())
qDebug("User Hash is not registered. Scan QR code to register.");
else {
qDebug() << "User Hash: " << userHash();
updateUserProjectList();
}
// Initialize background update
connect(&m_backgroundTimer, &QTimer::timeout, this, &Backend::updateUserProjectList);
m_backgroundTimer.setInterval(1000 * 10);
enableBackgroundUpdate(updateInBackground());
qDebug() << "Initialization complete";
}
void Backend::enableBackgroundUpdate(const bool &enabled)
{
if (enabled) {
m_backgroundTimer.start();
} else {
m_backgroundTimer.stop();
}
}
void Backend::setUpdateInBackground(const bool &enabled)
{
QSettings().setValue("system/updateInBackground", enabled);
if (enabled) {
qDebug() << "Update in background is enabled";
} else {
qDebug() << "Update in background is disabled";
}
enableBackgroundUpdate(enabled);
}
void Backend::setAutoScaleProject(const bool &enabled)
{
QSettings().setValue("system/autoScaleProject", enabled);
if (enabled) {
qDebug() << "Auto scale project is enabled";
} else {
qDebug() << "Auto scale project is disabled";
}
}
void Backend::setUserHash(const QString &userHash)
{
QSettings().setValue("user/hash", userHash);
emit userHashChanged();
}
bool Backend::updateInBackground()
{
return QSettings().value("system/updateInBackground", false).toBool();
}
bool Backend::autoScaleProject()
{
return QSettings().value("system/autoScaleProject", true).toBool();
}
QString Backend::userHash()
{
return QSettings().value("user/hash").toString();
}
void Backend::updatePopup(const QString &text, bool indeterminate)
{
emit popupTextChanged(text);
emit popupProgressIndeterminateChanged(indeterminate);
QEventLoop().processEvents(QEventLoop::AllEvents, 1000);
}
void Backend::scanQrCode()
{
// request permissions
QCameraPermission permission;
QCoreApplication &app = *QCoreApplication::instance();
switch (app.checkPermission(permission)) {
case Qt::PermissionStatus::Granted:
openCamera();
break;
case Qt::PermissionStatus::Undetermined:
case Qt::PermissionStatus::Denied:
app.requestPermission(permission, [this](const QPermission &permission) {
if (permission.status() == Qt::PermissionStatus::Denied) {
QMessageBox msgBox{QMessageBox::Critical,
"Critical:",
"Camera permission denied",
QMessageBox::Ok};
msgBox.exec();
return;
}
openCamera();
});
break;
}
}
void Backend::openCamera()
{
// start camera
m_captureSession.reset(new QMediaCaptureSession(this));
m_captureSession->setCamera(new QCamera(this));
m_captureSession->setVideoOutput(new CustomVideoWidget);
m_captureSession->camera()->setFocusMode(QCamera::FocusModeAuto);
CustomVideoWidget *videoWidget = qobject_cast<CustomVideoWidget *>(
m_captureSession->videoOutput());
connect(m_captureSession->videoSink(),
&QVideoSink::videoFrameChanged,
this,
[&](const QVideoFrame &frame) {
static int i = 0;
static QAtomicInt running = 0;
if (i++ < 20 || running > 0) {
return;
}
i = 0;
QtConcurrent::run([=] {
// lock the thread so that only one barcode can be read at a time
running++;
QElapsedTimer timer;
timer.start();
QList<Result> results = ReadBarcodes(frame.toImage());
qDebug() << "Barcode detection took" << timer.elapsed() << "ms";
// release the lock so that another barcode can be read
running--;
return results;
}).then([=](const QList<Result> &results) {
if (results.isEmpty() || !m_captureSession)
return;
qDebug() << "Stopping camera";
qobject_cast<CustomVideoWidget *>(m_captureSession->videoOutput())->close();
qDebug() << "Camera stopped";
Result result = results.first();
qDebug() << "Text: " << result.text();
qDebug() << "Format: " << result.format();
qDebug() << "Content:" << result.contentType();
// we have to use invokeMethod because we are in a different thread
// then where the serviceConnector is created
QMetaObject::invokeMethod(this,
"parseDesignViewerUrl",
Qt::QueuedConnection,
Q_ARG(QUrl, result.text()));
});
});
// stop camera when app is not active.
// i.e. when the user switches to another app or the screen is locked
QGuiApplication *app(qobject_cast<QGuiApplication *>(QCoreApplication::instance()));
connect(app, &QGuiApplication::applicationStateChanged, this, [&](Qt::ApplicationState state) {
if (state != Qt::ApplicationState::ApplicationActive && m_captureSession) {
qDebug() << "Application is not active. Stopping camera";
qobject_cast<CustomVideoWidget *>(m_captureSession->videoOutput())->close();
}
});
// stop camera when video widget is closed
connect(videoWidget, &CustomVideoWidget::closed, this, [&] {
qDebug() << "Video widget closed. Clearing capture session";
m_captureSession->camera()->stop();
m_captureSession->camera()->deleteLater();
m_captureSession->videoOutput()->deleteLater();
m_captureSession.clear();
});
videoWidget->setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint);
videoWidget->show();
m_captureSession->camera()->start();
}
void Backend::initializeProjectManager()
{
m_projectManager.reset(new ProjectManager(autoScaleProject()));
connect(m_projectManager.data(), &ProjectManager::closingProject, this, [&] {
emit popupClose();
m_projectManager.reset();
});
}
void Backend::initDesignStudioConnector()
{
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_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...");
if (!m_projectManager->runProject(projectPath)) {
qCritical() << "Could not run project. Please check the logs for more "
"information.";
} else {
m_projectManager->showAppWindow();
}
emit popupClose();
m_designStudioConnector->sendProjectReceived();
});
qDebug() << "Design Studio Connector is initialized";
}
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...");
m_projectManager.reset(new ProjectManager);
m_projectManager->clearDemoCaches();
m_projectManager.reset();
emit popupClose();
}
void Backend::runUserProject(const QString &projectName)
{
initializeProjectManager();
updatePopup("Running user project");
emit popupOpen();
qDebug() << "Running user project:" << projectName;
QString projectLastModified;
QString projectId;
QJsonObject projectInfo;
for (const auto &project : m_projectList) {
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);
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()) {
qWarning("User hash is not registered");
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;
}
}
// 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::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://")) {
qDebug() << "Registering user from QR code";
setUserHash(url.toString().remove("qtdesignviewer://"));
updateUserProjectList();
} else {
qWarning() << "Unknown QR code data: " << urlData;
}
}
/****************************************************************************
**
** 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 <QDesktopServices>
#include <QNetworkInterface>
#ifdef Q_OS_ANDROID
#include <QJniObject>
#endif
#include "logger.h"
#ifdef QT_DEBUG
#define buildType "Debug"
#else
#define buildType "Release"
#endif
#define FLAG_KEEP_SCREEN_ON 0x00000080
Backend::Backend(QObject *parent)
: QObject(parent)
{
// register the `qtdesignstudio` url handler to the Android system
QDesktopServices::setUrlHandler("qtdesignstudio", this, "parseDesignViewerUrl");
if (m_settings.deviceUuid().isEmpty()) {
qDebug() << "Device UUID not found. Generating a new one.";
m_settings.setDeviceUuid(QUuid::createUuid().toString(QUuid::WithoutBraces));
}
m_dsManagerThread.setParent(this);
connect(&m_dsManagerThread, &QThread::started, this, &Backend::initDsManager);
m_dsManagerThread.start();
m_projectManagerThread.setParent(this);
connect(&m_projectManagerThread, &QThread::started, this, &Backend::initProjectManager);
m_projectManagerThread.start();
connect(&Logger::instance(), &Logger::logMessage, this, [this](QtMsgType type, QString &msg) {
// if we have any active project running, then reroute
// all the logs to the dsmanager with the last project sender id
if (m_projectManager && !m_lastSessionId.isEmpty()
&& m_lastSessionId == m_projectManager->sessionId()) {
QMetaObject::invokeMethod(m_dsManager.get(),
&DesignStudioManager::sendProjectLogs,
Qt::QueuedConnection,
m_lastProjectSenderId,
msg);
}
});
connect(qApp,
&QGuiApplication::applicationStateChanged,
this,
[this](Qt::ApplicationState state) {
qDebug() << "Application state changed to" << state;
if (state == Qt::ApplicationState::ApplicationSuspended) {
if (m_projectManager && !m_projectManager->sessionId().isEmpty())
QMetaObject::invokeMethod(m_projectManager.get(),
&ProjectManager::stopProject);
m_dsManager->disconnectAllDesignStudios();
popupClose();
}
});
const QRect screenGeometry = QGuiApplication::primaryScreen()->geometry();
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() << "-- Device unique ID: " << m_settings.deviceUuid();
setAndroidScreenOn(keepScreenOn());
}
Backend::~Backend()
{
m_dsManagerThread.quit();
m_projectManagerThread.quit();
m_dsManagerThread.wait();
m_projectManagerThread.wait();
}
QString Backend::buildInfo() const
{
// clang-format off
return {
QCoreApplication::applicationVersion() +
"\n"+ CMAKE_VAR_GIT_VERSION +
"\nQt " + QT_VERSION_STR + " - " + buildType + " Build" +
"\nQt Quick Components " + CMAKE_VAR_QT_QUICK_COMPONENTS_VERSION +
"\nZXing-Cpp: " + CMAKE_VAR_ZXING_VERSION +
"\nOpenSSL support: " + QVariant(QSslSocket::supportsSsl()).toString()};
// clang-format on
}
void Backend::updatePopupText(const QString &text, int timeout)
{
emit popupOpen();
emit popupChangeText(text, timeout);
emit popupProgressIndeterminateChanged(true);
QEventLoop().processEvents(QEventLoop::AllEvents, 1000);
}
void Backend::updatePopupProgress(const int progress)
{
emit popupOpen();
emit popupProgressIndeterminateChanged(false);
emit popupChangeProgress(progress);
QEventLoop().processEvents(QEventLoop::AllEvents, 1000);
}
void Backend::initProjectManager()
{
m_projectManager.reset(new ProjectManager(this));
connect(m_projectManager.get(),
&ProjectManager::closingProject,
this,
[this](const QString &sessionId) {
// if seesion ids are same, it's most likely the project was running
// from the leftover session (DS connected, project started,
// DS disconnected without stopping the project)
// so we'll stop the project and do not send any signals to the DS
if (sessionId != m_lastSessionId)
return;
QMetaObject::invokeMethod(m_dsManager.get(),
&DesignStudioManager::sendProjectStopped,
Qt::QueuedConnection,
m_lastProjectSenderId);
});
}
void Backend::initDsManager()
{
qDebug() << "Design Studio Manager thread started. Initializing Design Studio Manager";
m_dsManager.reset(new DesignStudioManager(m_settings.deviceUuid()));
connect(m_dsManager.get(), &DesignStudioManager::projectReceived, this, &Backend::runProject);
connect(m_dsManager.get(),
&DesignStudioManager::designStudioConnected,
this,
[this](const QString &id, const QString &ipAddr) { emit connectedChanged(true); });
connect(m_dsManager.get(),
&DesignStudioManager::designStudioDisconnected,
this,
[this](const QString &id, const QString &ipAddr) {
if (id == m_lastProjectSenderId) {
QMetaObject::invokeMethod(m_projectManager.get(), &ProjectManager::stopProject);
}
emit popupClose();
});
connect(m_dsManager.get(), &DesignStudioManager::allDesignStudiosDisconnected, this, [this] {
qDebug() << "All Design Studios disconnected";
emit connectedChanged(false);
});
connect(m_dsManager.get(),
&DesignStudioManager::projectIncoming,
this,
[this](const QString &id, const int projectSize) {
qDebug() << "Project incoming with size" << projectSize;
emit updatePopupText("Receiving project...");
// we'll use this to notify the correct DS when the project started/stopped
m_lastProjectSenderId = id;
m_lastSessionId = QUuid::createUuid().toString(QUuid::WithoutBraces);
QMetaObject::invokeMethod(m_projectManager.get(),
&ProjectManager::stopProject,
Qt::QueuedConnection);
});
connect(m_dsManager.get(),
&DesignStudioManager::projectIncomingProgress,
this,
[this](const QString &id, const int percentage) { updatePopupProgress(percentage); });
connect(m_dsManager.get(),
&DesignStudioManager::projectStopRequested,
this,
[this](const QString &id) {
qDebug() << "Project stop requested";
emit popupClose();
QMetaObject::invokeMethod(m_projectManager.get(),
&ProjectManager::stopProject,
Qt::QueuedConnection);
QMetaObject::invokeMethod(m_dsManager.get(),
&DesignStudioManager::sendProjectStopped,
Qt::QueuedConnection,
m_lastProjectSenderId);
});
m_dsManager->init();
qDebug() << "Design Studio Manager initialized";
}
void Backend::runProject(const QString &id, const QByteArray &projectData)
{
emit updatePopupText("Running project...");
QTimer::singleShot(1000, [this, id, projectData] {
bool retVal;
QMetaObject::invokeMethod(m_projectManager.get(),
&ProjectManager::runProject,
Q_RETURN_ARG(bool, retVal),
projectData,
autoScaleProject(),
m_lastSessionId);
if (!retVal)
QMetaObject::invokeMethod(m_dsManager.get(),
&DesignStudioManager::sendProjectStopped,
id);
else {
QMetaObject::invokeMethod(m_projectManager.get(), &ProjectManager::showAppWindow);
QMetaObject::invokeMethod(m_dsManager.get(),
&DesignStudioManager::sendProjectStarted,
id);
}
emit popupClose();
});
}
void Backend::scanQrCode()
{
m_qrScanner.reset(new QrScanner);
connect(m_qrScanner.get(), &QrScanner::qrCodeScanned, this, [&](const QString &qrCode) {
qDebug() << "QR code scanned:" << qrCode;
m_qrScanner.reset();
parseDesignViewerUrl(QUrl(qrCode));
});
connect(m_qrScanner.get(), &QrScanner::windowClosed, this, [&]() { m_qrScanner.reset(); });
m_qrScanner->scanQrCode();
}
void Backend::parseDesignViewerUrl(const QUrl &url)
{
if (url.scheme() != "qtdesignstudio") {
qWarning() << "Unknown QR code format";
qWarning() << "URL:" << url.toString();
return;
}
if (url.host().isEmpty()) {
qWarning() << "No Design Studio IP address found in the QR code. URL:" << url.toString();
return;
}
// connectDesignStudio(url.host());
}
void Backend::popupInterrupted()
{
qDebug() << "Popup closed prematurely. Interrupting active downloads (if any)";
}
bool Backend::autoScaleProject() const
{
return m_settings.autoScaleProject();
}
void Backend::setAutoScaleProject(bool autoScaleProject)
{
m_settings.setAutoScaleProject(autoScaleProject);
}
bool Backend::keepScreenOn() const
{
return m_settings.keepScreenOn();
}
void Backend::setKeepScreenOn(bool keepScreenOn)
{
m_settings.setKeepScreenOn(keepScreenOn);
setAndroidScreenOn(keepScreenOn);
}
void Backend::setAndroidScreenOn(bool on)
{
#ifdef Q_OS_ANDROID
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([=]() {
QJniObject activity = QNativeInterface::QAndroidApplication::context();
QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;");
on ? window.callMethod<void>("addFlags", "(I)V", FLAG_KEEP_SCREEN_ON)
: window.callMethod<void>("clearFlags", "(I)V", FLAG_KEEP_SCREEN_ON);
});
#else
Q_UNUSED(on);
#endif
}
QString Backend::lastDesignStudioIp() const
{
return m_dsManager ? m_dsManager->getDesignStudioIp({}) : QString();
}
QJsonArray Backend::getIpAddresses() const
{
QNetworkInterface networkInterface;
QJsonArray ipAddresses;
for (const auto &interface : networkInterface.allInterfaces()) {
for (const auto &entry : interface.addressEntries()) {
if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol && !entry.ip().isLoopback()) {
ipAddresses.append(
QJsonObject{{"interface", interface.name()}, {"ip", entry.ip().toString()}});
}
}
}
return ipAddresses;
}
...@@ -26,120 +26,74 @@ ...@@ -26,120 +26,74 @@
#ifndef DV_ANDROID_H #ifndef DV_ANDROID_H
#define DV_ANDROID_H #define DV_ANDROID_H
#include <QCamera> #include <QJsonArray>
#include <QImageCapture>
#include <QMediaCaptureSession>
#include <QThread> #include <QThread>
#include <QVideoWidget>
#include <QWidget>
#include "dsConnector.h" #include "dsconnector/dsmanager.h"
#include "projectManager.h" #include "projectmanager.h"
#include "serviceConnector.h" #include "qrscanner.h"
#include "settings.h"
#include <QLabel>
#include <QVBoxLayout>
class CustomVideoWidget : public QVideoWidget
{
Q_OBJECT
public:
explicit CustomVideoWidget(QWidget *parent = nullptr)
: QVideoWidget(parent)
{}
~CustomVideoWidget() override {}
void closeEvent(QCloseEvent *event) override { emit closed(); }
signals:
void closed();
};
class Backend : public QObject class Backend : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QJsonArray projectList READ projectList NOTIFY projectListChanged)
public: public:
explicit Backend(QObject *parent = nullptr); explicit Backend(QObject *parent = nullptr);
void initialize(); ~Backend();
void setLogs(const QString &logs)
{
m_logs = logs;
emit logsChanged(m_logs);
}
QJsonArray projectList() const { return m_projectList; }
private: private:
// UI data
QString m_logs;
QJsonArray m_projectList;
// Other members
ServiceConnector m_serviceConnector;
QScopedPointer<ProjectManager> m_projectManager; QScopedPointer<ProjectManager> m_projectManager;
QScopedPointer<DesignStudioConnector> m_designStudioConnector; QThread m_projectManagerThread;
QThread m_dsConnectorThread;
QScopedPointer<DesignStudioManager> m_dsManager;
QThread m_dsManagerThread;
QScopedPointer<QrScanner> m_qrScanner;
// QR code scanner QString m_lastProjectSenderId;
QSharedPointer<QMediaCaptureSession> m_captureSession; QString m_lastSessionId;
// Settings // Settings
QTimer m_backgroundTimer; Settings m_settings;
// member functions // member functions
void updatePopup(const QString &text, bool indeterminate = true); void updatePopupText(const QString &text, int timeout = 0);
void updatePopupProgress(const int progress);
signals:
// UI signals - Home page
void projectListChanged();
void urlUpdated(QString);
void userHashChanged();
// UI signals - Logs page void initDsManager();
void logsChanged(QString); void initProjectManager();
void setAndroidScreenOn(bool on);
// UI signals - About page void runProject(const QString &id, const QByteArray &projectData);
void buildInfoChanged(QString);
signals:
// UI signals - Popup // UI signals - Popup
void downloadProgress(float);
void popupProgressIndeterminateChanged(bool indeterminate); void popupProgressIndeterminateChanged(bool indeterminate);
void popupTextChanged(QString text); void popupChangeText(QString text, int timeout = 0);
void popupChangeProgress(int progress);
void popupOpen(); void popupOpen();
void popupClose(); void popupClose();
// UI signals - Network page // UI signals - from DS Manager page
void networkUpdated(QString); void connectedChanged(bool connected);
// UI signals - from UI
void connectToDesignStudio(const QString &url);
public slots: public slots:
QString buildInfo() const;
void scanQrCode(); void scanQrCode();
void openCamera();
void runOnlineProject(const QString &url);
void runUserProject(const QString &projectName);
void runDemoProject(const QString &projectName);
void clearDemoCaches();
void initDesignStudioConnector();
void parseDesignViewerUrl(const QUrl &url); void parseDesignViewerUrl(const QUrl &url);
void popupInterrupted();
QJsonArray getIpAddresses() const;
// settings - setters bool autoScaleProject() const;
void setUpdateInBackground(const bool &enabled); void setAutoScaleProject(bool autoScaleProject);
void setAutoScaleProject(const bool &enabled);
void setUserHash(const QString &userHash);
// settings - getters bool keepScreenOn() const;
bool updateInBackground(); void setKeepScreenOn(bool keepScreenOn);
bool autoScaleProject();
QString userHash();
private slots: QString lastDesignStudioIp() const;
void initializeProjectManager();
void enableBackgroundUpdate(const bool &enabled);
void updateUserProjectList();
}; };
#endif // DV_ANDROID_H #endif // DV_ANDROID_H
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "ds.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QScreen>
namespace PackageFromDesignStudio {
using namespace Qt::Literals;
constexpr auto designStudioReady = "designStudioReady"_L1;
constexpr auto projectData = "projectData"_L1;
constexpr auto stopRunningProject = "stopRunningProject"_L1;
}; // namespace PackageFromDesignStudio
namespace PackageToDesignStudio {
using namespace Qt::Literals;
constexpr auto deviceInfo = "deviceInfo"_L1;
constexpr auto projectReceivingProgress = "projectReceivingProgress"_L1;
constexpr auto projectStarting = "projectStarting"_L1;
constexpr auto projectStarted = "projectRunning"_L1;
constexpr auto projectStopped = "projectStopped"_L1;
constexpr auto projectLogs = "projectLogs"_L1;
}; // namespace PackageToDesignStudio
DesignStudio::DesignStudio(QWebSocket *socket, const QString &deviceID, QObject *parent)
: QObject(parent)
, m_deviceID(deviceID)
, m_socket(socket)
{
initPingPong();
initSocket();
startPingPong();
m_projectStallTimer.setInterval(5000);
m_projectStallTimer.setSingleShot(true);
connect(&m_projectStallTimer, &QTimer::timeout, this, [this]() {
qDebug() << "Project is stalled. Closing the connection.";
m_socket->close();
m_socket->abort();
});
m_speedCalculator.setInterval(1000);
m_speedCalculator.setSingleShot(false);
connect(&m_speedCalculator, &QTimer::timeout, this, [this]() {
quint64 elapsedTime = m_projectNotificationTime.msecsTo(QTime::currentTime()) / 1000;
int receiveSpeed = elapsedTime > 0 ? m_projectData.size() / elapsedTime / 1024 : 0;
qDebug() << "Receive speed" << receiveSpeed << "KB/s (" << receiveSpeed / 1024 << "MB/s )"
<< "percentage" << m_lastPercentage << "%";
});
}
void DesignStudio::initPingPong()
{
m_pingTimer.setInterval(1000);
m_pongTimer.setInterval(30000);
m_pongTimer.setSingleShot(true);
m_pingTimer.setSingleShot(true);
connect(&m_pingTimer, &QTimer::timeout, this, [this]() {
m_socket->ping();
startPingPong();
});
connect(m_socket.data(),
&QWebSocket::pong,
this,
[this](quint64 elapsedTime, const QByteArray &) {
if (elapsedTime > 1000)
qWarning() << "Design Studio pong is too slow:" << elapsedTime
<< "ms. Newtork issue?";
else if (elapsedTime > 500)
qWarning() << "Design Studio pong is slow:" << elapsedTime << "ms";
m_pongTimer.stop();
m_pingTimer.start();
});
connect(&m_pongTimer, &QTimer::timeout, this, [this]() {
qDebug() << "Design Studio" << m_designStudioID
<< "is not responding. Closing the connection.";
m_socket->close();
m_socket->abort();
});
}
void DesignStudio::stopPingPong()
{
m_pingTimer.stop();
m_pongTimer.stop();
}
void DesignStudio::startPingPong()
{
m_pingTimer.start();
}
void DesignStudio::initSocket()
{
connect(m_socket.data(), &QWebSocket::disconnected, this, [this]() {
qDebug() << "Design Studio" << m_designStudioID << "disconnected";
m_pingTimer.stop();
m_pongTimer.stop();
m_projectStallTimer.stop();
m_speedCalculator.stop();
m_projectData.clear();
emit disconnected(m_designStudioID);
});
connect(m_socket.data(),
&QWebSocket::textMessageReceived,
this,
&DesignStudio::processTextMessage);
connect(m_socket.data(),
&QWebSocket::binaryMessageReceived,
this,
&DesignStudio::processBinaryMessage);
}
void DesignStudio::disconnect()
{
m_socket->close();
m_socket->abort();
}
QString DesignStudio::ipv4Addr() const
{
return m_socket->peerAddress().toString();
}
QString DesignStudio::id() const
{
return m_designStudioID;
}
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::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();
const QJsonObject data = jsonObj.value("data").toObject();
if (dataType == PackageFromDesignStudio::designStudioReady) {
const QString newDesignStudioId = data["designStudioID"].toString();
const int commVersion = data["commVersion"].toInt();
qDebug() << "Design Studio ready with ID" << newDesignStudioId << "and comm version"
<< commVersion;
m_designStudioID = newDesignStudioId;
sendDeviceInfo();
emit idReceived(newDesignStudioId, ipv4Addr());
} else if (dataType == PackageFromDesignStudio::projectData) {
m_incomingProjectSize = data["projectSize"].toInt();
const QString qtVersion = data["qtVersion"].toString();
auto qcompare = QString::compare(qtVersion, QT_VERSION_STR, Qt::CaseInsensitive);
if (qcompare != 0) {
qDebug() << "Qt version mismatch. Expected" << QT_VERSION_STR " or lower, but got"
<< qtVersion << ". Project may not work correctly.";
emit projectVersionMismatch(QT_VERSION_STR, qtVersion);
}
qDebug() << "Project is expected with size" << m_incomingProjectSize << "and Qt version"
<< qtVersion;
m_projectData.clear();
m_projectNotificationTime = QTime::currentTime();
m_speedCalculator.start();
m_projectStallTimer.start();
stopPingPong();
emit projectIncoming(m_designStudioID, m_incomingProjectSize);
} else if (dataType == PackageFromDesignStudio::stopRunningProject) {
qDebug() << "Stop running project requested";
m_projectData.clear();
m_speedCalculator.stop();
m_projectStallTimer.stop();
emit projectStopRequested(m_designStudioID);
} else {
qDebug() << "Unkown JSON message type:" << dataType;
}
}
void DesignStudio::processBinaryMessage(const QByteArray &data)
{
m_projectStallTimer.start();
m_projectData.append(data);
const bool isProjectComplete = m_projectData.size() == m_incomingProjectSize;
int percentage = m_projectData.size() * 100 / m_incomingProjectSize;
if (m_lastPercentage != percentage) {
emit projectIncomingProgress(m_designStudioID, percentage);
m_lastPercentage = percentage;
sendData(PackageToDesignStudio::projectReceivingProgress, percentage);
}
if (isProjectComplete) {
startPingPong();
sendProjectStarting();
emit projectReceived(m_designStudioID, m_projectData);
m_speedCalculator.stop();
m_projectStallTimer.stop();
}
}
void DesignStudio::sendProjectStarting()
{
sendData(PackageToDesignStudio::projectStarting);
}
void DesignStudio::sendProjectStarted()
{
sendData(PackageToDesignStudio::projectStarted);
}
void DesignStudio::sendProjectStopped()
{
sendData(PackageToDesignStudio::projectStopped);
}
void DesignStudio::sendProjectLogs(const QString &logs)
{
sendData(PackageToDesignStudio::projectLogs, logs);
}