From f3046da7d4abbdc3317030dcacab53a533998ef0 Mon Sep 17 00:00:00 2001 From: Burak Hancerli Date: Tue, 22 Oct 2024 15:44:01 +0200 Subject: [PATCH] DeviceShare: Add Android app connector and device manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: I30cb3dc8b71e87fc27482aa503cb53ce98c6bde0 Reviewed-by: Henning Gründl --- src/plugins/qmldesigner/CMakeLists.txt | 13 + .../components/devicesharing/device.cpp | 198 +++++++++++++ .../components/devicesharing/device.h | 64 +++++ .../components/devicesharing/deviceinfo.cpp | 130 +++++++++ .../components/devicesharing/deviceinfo.h | 81 ++++++ .../devicesharing/devicemanager.cpp | 260 ++++++++++++++++++ .../components/devicesharing/devicemanager.h | 69 +++++ .../devicesharing/devicemanagermodel.cpp | 162 +++++++++++ .../devicesharing/devicemanagermodel.h | 47 ++++ 9 files changed, 1024 insertions(+) create mode 100644 src/plugins/qmldesigner/components/devicesharing/device.cpp create mode 100644 src/plugins/qmldesigner/components/devicesharing/device.h create mode 100644 src/plugins/qmldesigner/components/devicesharing/deviceinfo.cpp create mode 100644 src/plugins/qmldesigner/components/devicesharing/deviceinfo.h create mode 100644 src/plugins/qmldesigner/components/devicesharing/devicemanager.cpp create mode 100644 src/plugins/qmldesigner/components/devicesharing/devicemanager.h create mode 100644 src/plugins/qmldesigner/components/devicesharing/devicemanagermodel.cpp create mode 100644 src/plugins/qmldesigner/components/devicesharing/devicemanagermodel.h diff --git a/src/plugins/qmldesigner/CMakeLists.txt b/src/plugins/qmldesigner/CMakeLists.txt index 4ee17a05dff..e19d2e3c226 100644 --- a/src/plugins/qmldesigner/CMakeLists.txt +++ b/src/plugins/qmldesigner/CMakeLists.txt @@ -34,6 +34,8 @@ add_feature_info("Meta info tracing" ${ENABLE_METAINFO_TRACING} "") add_subdirectory(libs) +find_package(Qt6 REQUIRED COMPONENTS WebSockets) + add_qtc_plugin(QmlDesigner PLUGIN_RECOMMENDS QmlPreview CONDITION TARGET QmlDesignerCore AND TARGET Qt::QuickWidgets AND TARGET Qt::Svg @@ -728,6 +730,17 @@ extend_qtc_plugin(QmlDesigner messagemodel.h ) +extend_qtc_plugin(QmlDesigner + SOURCES_PREFIX components/devicesharing + DEPENDS + Qt::WebSockets + SOURCES + device.cpp device.h + deviceinfo.cpp deviceinfo.h + devicemanager.cpp devicemanager.h + devicemanagermodel.cpp devicemanagermodel.h +) + add_qtc_plugin(assetexporterplugin PLUGIN_CLASS AssetExporterPlugin CONDITION TARGET QmlDesigner diff --git a/src/plugins/qmldesigner/components/devicesharing/device.cpp b/src/plugins/qmldesigner/components/devicesharing/device.cpp new file mode 100644 index 00000000000..7c3ebe37ddc --- /dev/null +++ b/src/plugins/qmldesigner/components/devicesharing/device.cpp @@ -0,0 +1,198 @@ +// 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 "device.h" + +#include +#include + +namespace QmlDesigner::DeviceShare { + +// Below are the constants that are used in the communication between the Design Studio and the device. +namespace PackageToDevice { +using namespace Qt::Literals; +constexpr auto designStudioReady = "designStudioReady"_L1; +constexpr auto projectData = "projectData"_L1; +constexpr auto stopRunningProject = "stopRunningProject"_L1; +}; // namespace PackageToDevice + +namespace PackageFromDevice { +using namespace Qt::Literals; +constexpr auto deviceInfo = "deviceInfo"_L1; +constexpr auto projectRunning = "projectRunning"_L1; +constexpr auto projectStopped = "projectStopped"_L1; +constexpr auto projectLogs = "projectLogs"_L1; +}; // namespace PackageFromDevice + +Device::Device(const DeviceInfo &deviceInfo, const DeviceSettings &deviceSettings, QObject *parent) + : QObject(parent) + , m_deviceInfo(deviceInfo) + , m_deviceSettings(deviceSettings) + , m_socket(nullptr) + , m_socketWasConnected(false) +{ + qCDebug(deviceSharePluginLog) << "initial device info:" << m_deviceInfo; + + m_socket.reset(new QWebSocket()); + connect(m_socket.data(), &QWebSocket::textMessageReceived, this, &Device::processTextMessage); + connect(m_socket.data(), &QWebSocket::disconnected, this, [this]() { + m_reconnectTimer.start(); + if (!m_socketWasConnected) + return; + + m_socketWasConnected = false; + m_pingTimer.stop(); + m_pongTimer.stop(); + emit disconnected(m_deviceInfo.deviceId()); + }); + connect(m_socket.data(), &QWebSocket::connected, this, [this]() { + m_socketWasConnected = true; + m_reconnectTimer.stop(); + m_pingTimer.start(15000); + sendDesignStudioReady(m_deviceInfo.deviceId()); + emit connected(m_deviceInfo.deviceId()); + }); + + m_reconnectTimer.setSingleShot(true); + m_reconnectTimer.setInterval(5000); + connect(&m_reconnectTimer, &QTimer::timeout, this, &Device::reconnect); + + initPingPong(); + reconnect(); +} + +Device::~Device() +{ + m_socket->close(); + m_socket.reset(); +} + +void Device::initPingPong() +{ + connect(&m_pingTimer, &QTimer::timeout, this, [this]() { + m_socket->ping(); + m_pongTimer.start(15000); + }); + + connect(m_socket.data(), + &QWebSocket::pong, + this, + [this](quint64 elapsedTime, [[maybe_unused]] const QByteArray &payload) { + qCDebug(deviceSharePluginLog) + << "Pong received from Device" << m_deviceInfo.deviceId() << "in" << elapsedTime + << "ms"; + m_pongTimer.stop(); + }); + + connect(&m_pongTimer, &QTimer::timeout, this, [this]() { + qCDebug(deviceSharePluginLog) + << "Device" << m_deviceInfo.deviceId() << "is not responding. Closing connection."; + m_socket->close(); + }); + + m_pongTimer.setSingleShot(true); +} + +void Device::reconnect() +{ + if (m_socket->state() == QAbstractSocket::ConnectedState) + m_socket->close(); + + QUrl url(QStringLiteral("ws://%1:%2").arg(m_deviceSettings.ipAddress()).arg(40000)); + m_socket->open(url); +} + +DeviceInfo Device::deviceInfo() const +{ + return m_deviceInfo; +} + +void Device::setDeviceInfo(const DeviceInfo &deviceInfo) +{ + m_deviceInfo = deviceInfo; +} + +DeviceSettings Device::deviceSettings() const +{ + return m_deviceSettings; +} + +void Device::setDeviceSettings(const DeviceSettings &deviceSettings) +{ + m_deviceSettings = deviceSettings; + reconnect(); +} + +void Device::sendDesignStudioReady(const QString &uuid) { + sendTextMessage(PackageToDevice::designStudioReady, uuid); +} + +void Device::sendProjectNotification() +{ + sendTextMessage(PackageToDevice::projectData); +} + +void Device::sendProjectData(const QByteArray &data) +{ + sendBinaryMessage(data); +} + +void Device::sendProjectStopped() +{ + sendTextMessage(PackageToDevice::stopRunningProject); +} + +bool Device::isConnected() const +{ + return m_socket ? m_socket->state() == QAbstractSocket::ConnectedState : false; +} + +void Device::sendTextMessage(const QLatin1String &dataType, const QJsonValue &data) +{ + if (!isConnected()) + return; + + QJsonObject message; + message["dataType"] = dataType; + message["data"] = data; + const QString jsonMessage = QString::fromLatin1( + QJsonDocument(message).toJson(QJsonDocument::Compact)); + m_socket->sendTextMessage(jsonMessage); +} + +void Device::sendBinaryMessage(const QByteArray &data) +{ + if (!isConnected()) + return; + + m_socket->sendBinaryMessage(data); +} + +void Device::processTextMessage(const QString &data) +{ + QJsonParseError jsonError; + const QJsonDocument jsonDoc = QJsonDocument::fromJson(data.toLatin1(), &jsonError); + if (jsonError.error != QJsonParseError::NoError) { + qCDebug(deviceSharePluginLog) + << "Failed to parse JSON message:" << jsonError.errorString() << data; + return; + } + + const QJsonObject jsonObj = jsonDoc.object(); + const QString dataType = jsonObj.value("dataType").toString(); + if (dataType == PackageFromDevice::deviceInfo) { + QJsonObject deviceInfo = jsonObj.value("data").toObject(); + m_deviceInfo.setJsonObject(deviceInfo); + emit deviceInfoReady(m_deviceInfo.deviceId(), m_deviceInfo); + } else if (dataType == PackageFromDevice::projectRunning) { + emit projectStarted(m_deviceInfo.deviceId()); + } else if (dataType == PackageFromDevice::projectStopped) { + emit projectStopped(m_deviceInfo.deviceId()); + } else if (dataType == PackageFromDevice::projectLogs) { + emit projectLogsReceived(m_deviceInfo.deviceId(), jsonObj.value("data").toString()); + } else { + qCDebug(deviceSharePluginLog) << "Invalid JSON message:" << jsonObj; + } +} + +} // namespace QmlDesigner::DeviceShare diff --git a/src/plugins/qmldesigner/components/devicesharing/device.h b/src/plugins/qmldesigner/components/devicesharing/device.h new file mode 100644 index 00000000000..1d9042893de --- /dev/null +++ b/src/plugins/qmldesigner/components/devicesharing/device.h @@ -0,0 +1,64 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include +#include + +#include "deviceinfo.h" + +namespace QmlDesigner::DeviceShare { + +class Device : public QObject +{ + Q_OBJECT +public: + Device(const DeviceInfo &deviceInfo = {}, + const DeviceSettings &deviceSettings = {}, + QObject *parent = nullptr); + ~Device(); + + // device management + DeviceInfo deviceInfo() const; + DeviceSettings deviceSettings() const; + void setDeviceInfo(const DeviceInfo &deviceInfo); + void setDeviceSettings(const DeviceSettings &deviceSettings); + + // device communication + void sendDesignStudioReady(const QString &uuid); + void sendProjectNotification(); + void sendProjectData(const QByteArray &data); + void sendProjectStopped(); + + // socket + bool isConnected() const; + void reconnect(); + +private slots: + void processTextMessage(const QString &data); + +private: + DeviceInfo m_deviceInfo; + DeviceSettings m_deviceSettings; + + QScopedPointer m_socket; + bool m_socketWasConnected; + + QTimer m_reconnectTimer; + QTimer m_pingTimer; + QTimer m_pongTimer; + + void initPingPong(); + void sendTextMessage(const QLatin1String &dataType, const QJsonValue &data = QJsonValue()); + void sendBinaryMessage(const QByteArray &data); + +signals: + void connected(const QString &deviceId); + void disconnected(const QString &deviceId); + void deviceInfoReady(const QString &deviceId, const DeviceInfo &deviceInfo); + void projectStarted(const QString &deviceId); + void projectStopped(const QString &deviceId); + void projectLogsReceived(const QString &deviceId, const QString &logs); +}; +} // namespace QmlDesigner::DeviceShare diff --git a/src/plugins/qmldesigner/components/devicesharing/deviceinfo.cpp b/src/plugins/qmldesigner/components/devicesharing/deviceinfo.cpp new file mode 100644 index 00000000000..f2557100f09 --- /dev/null +++ b/src/plugins/qmldesigner/components/devicesharing/deviceinfo.cpp @@ -0,0 +1,130 @@ +// 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 "deviceinfo.h" + +#include + +namespace QmlDesigner::DeviceShare { +Q_LOGGING_CATEGORY(deviceSharePluginLog, "qtc.designer.deviceSharePluginLog") + +IDeviceData::IDeviceData(const QJsonObject &data) + : m_data(data) +{} + +QJsonObject IDeviceData::jsonObject() const +{ + return m_data; +} + +void IDeviceData::setJsonObject(const QJsonObject &data) +{ + m_data = data; +} + +IDeviceData::operator QString() const +{ + return QString::fromLatin1(QJsonDocument(m_data).toJson(QJsonDocument::Compact)); +} + +bool DeviceSettings::active() const +{ + return m_data.value(keyActive).toBool(); +} + +QString DeviceSettings::alias() const +{ + return m_data.value(keyAlias).toString(); +} + +QString DeviceSettings::ipAddress() const +{ + return m_data.value(keyIpAddress).toString(); +} + +void DeviceSettings::setActive(const bool &active) +{ + m_data[keyActive] = active; +} + +void DeviceSettings::setAlias(const QString &alias) +{ + m_data[keyAlias] = alias; +} + +void DeviceSettings::setIpAddress(const QString &ipAddress) +{ + m_data[keyIpAddress] = ipAddress; +} + +QString DeviceInfo::os() const +{ + return m_data.value(keyOs).toString(); +} + +QString DeviceInfo::osVersion() const +{ + return m_data.value(keyOsVersion).toString(); +} + +QString DeviceInfo::architecture() const +{ + return m_data.value(keyArchitecture).toString(); +} + +int DeviceInfo::screenWidth() const +{ + return m_data.value(keyScreenWidth).toInt(); +} + +int DeviceInfo::screenHeight() const +{ + return m_data.value(keyScreenHeight).toInt(); +} + +QString DeviceInfo::deviceId() const +{ + return m_data.value(keyDeviceId).toString(); +} + +QString DeviceInfo::appVersion() const +{ + return m_data.value(keyAppVersion).toString(); +} + +void DeviceInfo::setOs(const QString &os) +{ + m_data[keyOs] = os; +} + +void DeviceInfo::setOsVersion(const QString &osVersion) +{ + m_data[keyOsVersion] = osVersion; +} + +void DeviceInfo::setArchitecture(const QString &architecture) +{ + m_data[keyArchitecture] = architecture; +} + +void DeviceInfo::setScreenWidth(const int &screenWidth) +{ + m_data[keyScreenWidth] = screenWidth; +} + +void DeviceInfo::setScreenHeight(const int &screenHeight) +{ + m_data[keyScreenHeight] = screenHeight; +} + +void DeviceInfo::setDeviceId(const QString &deviceId) +{ + m_data[keyDeviceId] = deviceId; +} + +void DeviceInfo::setAppVersion(const QString &appVersion) +{ + m_data[keyAppVersion] = appVersion; +} + +} // namespace QmlDesigner::DeviceShare diff --git a/src/plugins/qmldesigner/components/devicesharing/deviceinfo.h b/src/plugins/qmldesigner/components/devicesharing/deviceinfo.h new file mode 100644 index 00000000000..cc79e546795 --- /dev/null +++ b/src/plugins/qmldesigner/components/devicesharing/deviceinfo.h @@ -0,0 +1,81 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include +#include + +namespace QmlDesigner::DeviceShare { +Q_DECLARE_LOGGING_CATEGORY(deviceSharePluginLog); + +class IDeviceData +{ +public: + IDeviceData(const QJsonObject &data = QJsonObject()); + virtual ~IDeviceData() = default; + + // converters + QJsonObject jsonObject() const; + void setJsonObject(const QJsonObject &data); + operator QString() const; + +protected: + QJsonObject m_data; +}; + +class DeviceSettings : public IDeviceData +{ +public: + DeviceSettings() = default; + + // Getters + bool active() const; + QString alias() const; + QString ipAddress() const; + + // Setters + void setActive(const bool &active); + void setAlias(const QString &alias); + void setIpAddress(const QString &ipAddress); + +private: + static constexpr char keyActive[] = "deviceActive"; + static constexpr char keyAlias[] = "deviceAlias"; + static constexpr char keyIpAddress[] = "ipAddress"; +}; + +class DeviceInfo : public IDeviceData +{ +public: + DeviceInfo() = default; + + // Getters + QString os() const; + QString osVersion() const; + QString architecture() const; + int screenWidth() const; + int screenHeight() const; + QString deviceId() const; + QString appVersion() const; + + // Setters + void setOs(const QString &os); + void setOsVersion(const QString &osVersion); + void setArchitecture(const QString &architecture); + void setScreenWidth(const int &screenWidth); + void setScreenHeight(const int &screenHeight); + void setDeviceId(const QString &deviceId); + void setAppVersion(const QString &appVersion); + +private: + static constexpr char keyOs[] = "os"; + static constexpr char keyOsVersion[] = "osVersion"; + static constexpr char keyScreenWidth[] = "screenWidth"; + static constexpr char keyScreenHeight[] = "screenHeight"; + static constexpr char keyDeviceId[] = "deviceId"; + static constexpr char keyArchitecture[] = "architecture"; + static constexpr char keyAppVersion[] = "appVersion"; +}; + +} // namespace QmlDesigner::DeviceShare diff --git a/src/plugins/qmldesigner/components/devicesharing/devicemanager.cpp b/src/plugins/qmldesigner/components/devicesharing/devicemanager.cpp new file mode 100644 index 00000000000..79fefa2e010 --- /dev/null +++ b/src/plugins/qmldesigner/components/devicesharing/devicemanager.cpp @@ -0,0 +1,260 @@ +// 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 "devicemanager.h" + +#include +#include +#include +#include +#include +#include + +namespace QmlDesigner::DeviceShare { + +DeviceManager::DeviceManager(QObject *parent, const QString &settingsPath) + : QObject(parent) + , m_settingsPath(settingsPath) +{ + readSettings(); + if (m_uuid.isEmpty()) { + m_uuid = QUuid::createUuid().toString(QUuid::WithoutBraces); + writeSettings(); + } +} + +void DeviceManager::writeSettings() +{ + QJsonObject root; + QJsonArray devices; + for (const auto &device : m_devices) { + QJsonObject deviceInfo; + deviceInfo.insert("deviceInfo", device->deviceInfo().jsonObject()); + deviceInfo.insert("deviceSettings", device->deviceSettings().jsonObject()); + devices.append(deviceInfo); + } + + root.insert("devices", devices); + root.insert("uuid", m_uuid); + + QJsonDocument doc(root); + QFile file(m_settingsPath); + if (!file.open(QIODevice::WriteOnly)) { + qCWarning(deviceSharePluginLog) << "Failed to open settings file" << file.fileName(); + return; + } + + file.write(doc.toJson()); +} + +void DeviceManager::readSettings() +{ + QFile file(m_settingsPath); + qCDebug(deviceSharePluginLog) << "Reading settings from" << file.fileName(); + if (!file.open(QIODevice::ReadOnly)) { + qCWarning(deviceSharePluginLog) << "Failed to open settings file" << file.fileName(); + return; + } + + QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + m_uuid = doc.object()["uuid"].toString(); + QJsonArray devices = doc.object()["devices"].toArray(); + for (const QJsonValue &deviceInfoJson : devices) { + DeviceInfo deviceInfo; + DeviceSettings deviceSettings; + deviceInfo.setJsonObject(deviceInfoJson.toObject()["deviceInfo"].toObject()); + deviceSettings.setJsonObject(deviceInfoJson.toObject()["deviceSettings"].toObject()); + auto device = initDevice(deviceInfo, deviceSettings); + m_devices.append(device); + } +} + +QList> DeviceManager::devices() const +{ + return m_devices; +} + +QSharedPointer DeviceManager::findDevice(const QString &deviceId) const +{ + auto it = std::find_if(m_devices.begin(), m_devices.end(), [deviceId](const auto &device) { + return device->deviceInfo().deviceId() == deviceId; + }); + + return it != m_devices.end() ? *it : nullptr; +} + +std::optional DeviceManager::deviceInfo(const QString &deviceId) const +{ + auto device = findDevice(deviceId); + if (!device) + return {}; + + return device->deviceInfo(); +} + +void DeviceManager::setDeviceAlias(const QString &deviceId, const QString &alias) +{ + auto device = findDevice(deviceId); + if (!device) + return; + + auto deviceSettings = device->deviceSettings(); + deviceSettings.setAlias(alias); + device->setDeviceSettings(deviceSettings); + writeSettings(); +} + +void DeviceManager::setDeviceActive(const QString &deviceId, const bool active) +{ + auto device = findDevice(deviceId); + if (!device) + return; + + auto deviceSettings = device->deviceSettings(); + deviceSettings.setActive(active); + device->setDeviceSettings(deviceSettings); + writeSettings(); +} + +void DeviceManager::setDeviceIP(const QString &deviceId, const QString &ip) +{ + auto device = findDevice(deviceId); + if (!device) + return; + + auto deviceSettings = device->deviceSettings(); + deviceSettings.setIpAddress(ip); + device->setDeviceSettings(deviceSettings); + writeSettings(); +} + +void DeviceManager::addDevice(const QString &ip) +{ + if (ip.isEmpty()) + return; + + const auto trimmedIp = ip.trimmed(); + + // check regex for xxx.xxx.xxx.xxx + QRegularExpression ipRegex(R"(^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$)"); + if (!ipRegex.match(trimmedIp).hasMatch()) { + qCWarning(deviceSharePluginLog) << "Invalid IP address" << ip; + return; + } + + for (const auto &device : m_devices) { + if (device->deviceSettings().ipAddress() == trimmedIp) { + qCWarning(deviceSharePluginLog) << "Device" << trimmedIp << "already exists"; + return; + } + } + + DeviceSettings deviceSettings; + deviceSettings.setIpAddress(trimmedIp); + auto device = initDevice({}, deviceSettings); + m_devices.append(device); + writeSettings(); + emit deviceAdded(device->deviceInfo()); +} + +QSharedPointer DeviceManager::initDevice(const DeviceInfo &deviceInfo, + const DeviceSettings &deviceSettings) +{ + QSharedPointer device = QSharedPointer(new Device{deviceInfo, deviceSettings}, + &QObject::deleteLater); + connect(device.data(), &Device::deviceInfoReady, this, &DeviceManager::deviceInfoReceived); + connect(device.data(), &Device::disconnected, this, &DeviceManager::deviceDisconnected); + connect(device.data(), &Device::projectStarted, this, [this](const QString deviceId) { + auto device = findDevice(deviceId); + qCDebug(deviceSharePluginLog) << "Project started on device" << deviceId; + emit projectStarted(device->deviceInfo()); + }); + connect(device.data(), &Device::projectStopped, this, [this](const QString deviceId) { + auto device = findDevice(deviceId); + qCDebug(deviceSharePluginLog) << "Project stopped on device" << deviceId; + emit projectStopped(device->deviceInfo()); + }); + connect(device.data(), + &Device::projectLogsReceived, + this, + [this](const QString deviceId, const QString &logs) { + auto device = findDevice(deviceId); + qCDebug(deviceSharePluginLog) << "Log:" << deviceId << logs; + emit projectLogsReceived(device->deviceInfo(), logs); + }); + + return device; +} + +void DeviceManager::deviceInfoReceived(const QString &deviceId, const DeviceInfo &deviceInfo) +{ + writeSettings(); + qCDebug(deviceSharePluginLog) << "Device" << deviceId << "is online"; + emit deviceOnline(deviceInfo); +} + +void DeviceManager::deviceDisconnected(const QString &deviceId) +{ + auto device = findDevice(deviceId); + if (!device) + return; + + qCDebug(deviceSharePluginLog) << "Device" << deviceId << "disconnected"; + emit deviceOffline(device->deviceInfo()); +} + +void DeviceManager::removeDevice(const QString &deviceId) +{ + auto device = findDevice(deviceId); + if (!device) + return; + + const auto deviceInfo = device->deviceInfo(); + m_devices.removeOne(device); + writeSettings(); + emit deviceRemoved(deviceInfo); +} + +void DeviceManager::removeDeviceAt(int index) +{ + if (index < 0 || index >= m_devices.size()) + return; + + auto deviceInfo = m_devices[index]->deviceInfo(); + m_devices.removeAt(index); + writeSettings(); + emit deviceRemoved(deviceInfo); +} + +void DeviceManager::sendProjectFile(const QString &deviceId, const QString &projectFile) +{ + auto device = findDevice(deviceId); + if (!device) + return; + + device->sendProjectNotification(); + + QFile file(projectFile); + if (!file.open(QIODevice::ReadOnly)) { + qCWarning(deviceSharePluginLog) << "Failed to open project file" << projectFile; + return; + } + + qCDebug(deviceSharePluginLog) << "Sending project file to device" << deviceId; + + QByteArray projectData = file.readAll(); + device->sendProjectData(projectData); + + qCDebug(deviceSharePluginLog) << "Project file sent to device" << deviceId; +} + +void DeviceManager::stopRunningProject(const QString &deviceId) +{ + auto device = findDevice(deviceId); + if (!device) + return; + + device->sendProjectStopped(); +} + +} // namespace QmlDesigner::DeviceShare diff --git a/src/plugins/qmldesigner/components/devicesharing/devicemanager.h b/src/plugins/qmldesigner/components/devicesharing/devicemanager.h new file mode 100644 index 00000000000..bd3484b6d18 --- /dev/null +++ b/src/plugins/qmldesigner/components/devicesharing/devicemanager.h @@ -0,0 +1,69 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include +#include +#include + +#include "device.h" + +namespace QmlDesigner::DeviceShare { + +class DeviceManager : public QObject +{ + Q_OBJECT +public: + explicit DeviceManager(QObject *parent = nullptr, const QString &settingsPath = "settings.json"); + + // internal init functions + void addDevice(const QString &ip); + + // Getters + QList> devices() const; + std::optional deviceInfo(const QString &deviceId) const; + + // Device management functions + void setDeviceAlias(const QString &deviceId, const QString &alias); + void setDeviceActive(const QString &deviceId, const bool active); + void setDeviceIP(const QString &deviceId, const QString &ip); + + void removeDevice(const QString &deviceId); + void removeDeviceAt(int index); + void sendProjectFile(const QString &deviceId, const QString &projectFile); + void stopRunningProject(const QString &deviceId); + +private: + // Devices management + QList> m_devices; + + // settings + const QString m_settingsPath; + QString m_uuid; + +private: + // internal slots + void incomingConnection(); + void readSettings(); + void writeSettings(); + QSharedPointer initDevice(const DeviceInfo &deviceInfo = DeviceInfo(), + const DeviceSettings &deviceSettings = DeviceSettings()); + + // device signals + void deviceInfoReceived(const QString &deviceId, const DeviceInfo &deviceInfo); + void deviceDisconnected(const QString &deviceId); + + QSharedPointer findDevice(const QString &deviceId) const; + +signals: + void deviceAdded(const DeviceInfo &deviceInfo); + void deviceRemoved(const DeviceInfo &deviceInfo); + void deviceOnline(const DeviceInfo &deviceInfo); + void deviceOffline(const DeviceInfo &deviceInfo); + void projectStarted(const DeviceInfo &deviceInfo); + void projectStopped(const DeviceInfo &deviceInfo); + void projectLogsReceived(const DeviceInfo &deviceInfo, const QString &logs); +}; + +} // namespace QmlDesigner::DeviceShare diff --git a/src/plugins/qmldesigner/components/devicesharing/devicemanagermodel.cpp b/src/plugins/qmldesigner/components/devicesharing/devicemanagermodel.cpp new file mode 100644 index 00000000000..6614cb3cf48 --- /dev/null +++ b/src/plugins/qmldesigner/components/devicesharing/devicemanagermodel.cpp @@ -0,0 +1,162 @@ +// 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 "devicemanagermodel.h" +#include "devicemanager.h" + +namespace QmlDesigner::DeviceShare { + +DeviceManagerModel::DeviceManagerModel(DeviceManager &deviceManager, QObject *parent) + : QAbstractTableModel(parent) + , m_deviceManager(deviceManager) +{ + connect(&m_deviceManager, &DeviceManager::deviceAdded, this, [this](const DeviceInfo &) { + endResetModel(); + }); + connect(&m_deviceManager, &DeviceManager::deviceRemoved, this, [this](const DeviceInfo &) { + endResetModel(); + }); + + connect(&m_deviceManager, &DeviceManager::deviceOnline, this, [this](const DeviceInfo &) { + endResetModel(); + }); + + connect(&m_deviceManager, &DeviceManager::deviceOffline, this, [this](const DeviceInfo &) { + endResetModel(); + }); +} + +int DeviceManagerModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return m_deviceManager.devices().size(); +} + +int DeviceManagerModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return DeviceColumns::COLUMN_COUNT; +} + +QVariant DeviceManagerModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (role == Qt::DisplayRole) { + const auto deviceInfo = m_deviceManager.devices()[index.row()]->deviceInfo(); + const auto deviceSettings = m_deviceManager.devices()[index.row()]->deviceSettings(); + bool isConnected = m_deviceManager.devices()[index.row()]->isConnected(); + switch (index.column()) { + case DeviceColumns::Active: + return deviceSettings.active(); + case DeviceColumns::Status: + return static_cast(isConnected); + case DeviceColumns::Alias: + return deviceSettings.alias(); + case DeviceColumns::IPv4Addr: + return deviceSettings.ipAddress(); + case DeviceColumns::OS: + return deviceInfo.os(); + case DeviceColumns::OSVersion: + return deviceInfo.osVersion(); + case DeviceColumns::Architecture: + return deviceInfo.architecture(); + case DeviceColumns::ScreenSize: + return QString("%1x%2").arg(deviceInfo.screenWidth()).arg(deviceInfo.screenHeight()); + case DeviceColumns::AppVersion: + return deviceInfo.appVersion(); + case DeviceColumns::DeviceId: + return deviceInfo.deviceId(); + } + } + + if (role == Qt::EditRole) { + const auto deviceSettings = m_deviceManager.devices()[index.row()]->deviceSettings(); + switch (index.column()) { + case DeviceColumns::Alias: + return deviceSettings.alias(); + case DeviceColumns::Active: + return deviceSettings.active(); + } + } + + return QVariant(); +} + +QVariant DeviceManagerModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + if (orientation == Qt::Horizontal) { + switch (section) { + case DeviceColumns::Active: + return "Active"; + case DeviceColumns::Status: + return "Status"; + case DeviceColumns::Alias: + return "Alias"; + case DeviceColumns::IPv4Addr: + return "IPv4 Address"; + case DeviceColumns::OS: + return "OS"; + case DeviceColumns::OSVersion: + return "OS Version"; + case DeviceColumns::Architecture: + return "Architecture"; + case DeviceColumns::ScreenSize: + return "Screen Size"; + case DeviceColumns::AppVersion: + return "App Version"; + case DeviceColumns::DeviceId: + return "Device ID"; + case DeviceColumns::Remove: + return "Remove"; + } + } + + return QVariant(); +} + +bool DeviceManagerModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || role != Qt::EditRole) { + qCWarning(deviceSharePluginLog) << "Invalid index or role"; + return false; + } + + auto deviceInfo = m_deviceManager.devices()[index.row()]->deviceInfo(); + switch (index.column()) { + case DeviceColumns::Alias: + m_deviceManager.setDeviceAlias(deviceInfo.deviceId(), value.toString()); + break; + case DeviceColumns::Active: + m_deviceManager.setDeviceActive(deviceInfo.deviceId(), value.toBool()); + break; + case DeviceColumns::IPv4Addr: + m_deviceManager.setDeviceIP(deviceInfo.deviceId(), value.toString()); + break; + } + emit dataChanged(index, index, {role}); + return true; +} + +Qt::ItemFlags DeviceManagerModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + + if (index.column() == DeviceColumns::Active || index.column() == DeviceColumns::Alias) + flags |= Qt::ItemIsEditable; + + return flags; +} + +} // namespace QmlDesigner::DeviceShare diff --git a/src/plugins/qmldesigner/components/devicesharing/devicemanagermodel.h b/src/plugins/qmldesigner/components/devicesharing/devicemanagermodel.h new file mode 100644 index 00000000000..c3c676d02bd --- /dev/null +++ b/src/plugins/qmldesigner/components/devicesharing/devicemanagermodel.h @@ -0,0 +1,47 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +namespace QmlDesigner::DeviceShare { + +class DeviceManager; +class DeviceManagerModel : public QAbstractTableModel +{ + Q_OBJECT +public: + explicit DeviceManagerModel(DeviceManager &deviceManager, QObject *parent = nullptr); + + enum DeviceStatus { Offline, Online }; + Q_ENUM(DeviceStatus) + + enum DeviceColumns { + Active, + Status, + Alias, + IPv4Addr, + OS, + OSVersion, + Architecture, + ScreenSize, + AppVersion, + DeviceId, + Remove, + COLUMN_COUNT + }; + Q_ENUM(DeviceColumns) + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + +private: + DeviceManager &m_deviceManager; +}; + +} // namespace QmlDesigner::DeviceShare