From 7ebb4aa37ca0fb15d86663b5c882f22228ba8991 Mon Sep 17 00:00:00 2001 From: Florian RICHER Date: Wed, 6 Aug 2025 20:06:17 +0000 Subject: [PATCH] waydroid: Migrate to DBus backend implementations --- .../waydroidintegrationplugin/CMakeLists.txt | 44 +- .../waydroidapplication.cpp | 69 -- .../waydroidapplication.h | 39 - .../waydroidapplicationdbusclient.cpp | 103 +++ .../waydroidapplicationdbusclient.h | 58 ++ .../waydroidapplicationdbusobject.cpp | 110 +++ .../waydroidapplicationdbusobject.h | 64 ++ .../waydroidapplicationlistmodel.cpp | 190 ++--- .../waydroidapplicationlistmodel.h | 23 +- .../waydroiddbusclient.cpp | 404 +++++++++ .../waydroiddbusclient.h | 173 ++++ ...ydroidstate.cpp => waydroiddbusobject.cpp} | 776 ++++++++++-------- .../{waydroidstate.h => waydroiddbusobject.h} | 130 ++- .../ui/WaydroidApplicationsPage.qml | 18 +- .../ui/WaydroidConfigurationForm.qml | 28 +- .../ui/WaydroidErrorPage.qml | 39 + ...roidGooglePlayProtectConfigurationPage.qml | 10 +- .../ui/WaydroidInitialConfigurationForm.qml | 6 +- kcms/waydroidintegration/ui/main.qml | 52 +- quicksettings/waydroid/contents/ui/main.qml | 30 +- 20 files changed, 1622 insertions(+), 744 deletions(-) delete mode 100644 components/waydroidintegrationplugin/waydroidapplication.cpp delete mode 100644 components/waydroidintegrationplugin/waydroidapplication.h create mode 100644 components/waydroidintegrationplugin/waydroidapplicationdbusclient.cpp create mode 100644 components/waydroidintegrationplugin/waydroidapplicationdbusclient.h create mode 100644 components/waydroidintegrationplugin/waydroidapplicationdbusobject.cpp create mode 100644 components/waydroidintegrationplugin/waydroidapplicationdbusobject.h create mode 100644 components/waydroidintegrationplugin/waydroiddbusclient.cpp create mode 100644 components/waydroidintegrationplugin/waydroiddbusclient.h rename components/waydroidintegrationplugin/{waydroidstate.cpp => waydroiddbusobject.cpp} (54%) rename components/waydroidintegrationplugin/{waydroidstate.h => waydroiddbusobject.h} (60%) create mode 100644 kcms/waydroidintegration/ui/WaydroidErrorPage.qml diff --git a/components/waydroidintegrationplugin/CMakeLists.txt b/components/waydroidintegrationplugin/CMakeLists.txt index 2ae3c28d..adffd718 100644 --- a/components/waydroidintegrationplugin/CMakeLists.txt +++ b/components/waydroidintegrationplugin/CMakeLists.txt @@ -1,10 +1,51 @@ # SPDX-FileCopyrightText: 2025 Florian RICHER # SPDX-License-Identifier: BSD-2-Clause +set(waydroidintegrationplugin_SRCS + waydroidapplicationdbusobject.cpp + waydroidapplicationdbusclient.cpp + waydroidapplicationlistmodel.cpp + waydroiddbusclient.cpp + waydroiddbusobject.cpp +) + +qt_generate_dbus_interface( + ${CMAKE_CURRENT_SOURCE_DIR}/waydroiddbusobject.h + org.kde.plasmashell.Waydroid.xml + OPTIONS -s -m -P +) +qt_generate_dbus_interface( + ${CMAKE_CURRENT_SOURCE_DIR}/waydroidapplicationdbusobject.h + org.kde.plasmashell.WaydroidApplication.xml + OPTIONS -s -m -P +) +qt_add_dbus_adaptor(waydroidintegrationplugin_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.Waydroid.xml + ${CMAKE_CURRENT_SOURCE_DIR}/waydroiddbusobject.h WaydroidDBusObject +) +qt_add_dbus_adaptor(waydroidintegrationplugin_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.WaydroidApplication.xml + ${CMAKE_CURRENT_SOURCE_DIR}/waydroidapplicationdbusobject.h WaydroidApplicationDBusObject +) +qt_add_dbus_interface(waydroidintegrationplugin_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.Waydroid.xml + plasmashellwaydroidinterface +) +qt_add_dbus_interface(waydroidintegrationplugin_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.WaydroidApplication.xml + plasmashellwaydroidapplicationinterface +) +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.Waydroid.xml + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.WaydroidApplication.xml + DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR} +) + ecm_add_qml_module(waydroidintegrationplugin URI org.kde.plasma.private.mobileshell.waydroidintegrationplugin GENERATE_PLUGIN_SOURCE) -target_sources(waydroidintegrationplugin PRIVATE waydroidstate.cpp waydroidapplication.cpp waydroidapplicationlistmodel.cpp) +target_sources(waydroidintegrationplugin PRIVATE ${waydroidintegrationplugin_SRCS} ${RESOURCES}) target_link_libraries(waydroidintegrationplugin PRIVATE + Qt::DBus Qt::Gui Qt::Qml Qt::Quick @@ -12,6 +53,7 @@ target_link_libraries(waydroidintegrationplugin PRIVATE KF6::ConfigCore KF6::I18n QCoro::Core + QCoro::DBus QCoro::Qml ) diff --git a/components/waydroidintegrationplugin/waydroidapplication.cpp b/components/waydroidintegrationplugin/waydroidapplication.cpp deleted file mode 100644 index 08278544..00000000 --- a/components/waydroidintegrationplugin/waydroidapplication.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Florian RICHER - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include - -#include -#include - -using namespace Qt::StringLiterals; - -static const QRegularExpression nameRegExp(u"^Name:\\s*(\\S+)"_s); -static const QRegularExpression packageNameRegExp(u"^packageName:\\s*(\\S+)"_s); - -WaydroidApplication::WaydroidApplication(QObject *parent) - : QObject{parent} -{ - // Nothing -} - -WaydroidApplication::Ptr WaydroidApplication::fromWaydroidLog(QTextStream &inFile) -{ - WaydroidApplication::Ptr app; - - const QString line = inFile.readLine(); - const QRegularExpressionMatch nameMatch = nameRegExp.match(line); - - if (!nameMatch.hasMatch() || nameMatch.lastCapturedIndex() == 0) { - return nullptr; - } - - app = std::make_shared(); - app->m_name = nameMatch.captured(nameMatch.lastCapturedIndex()); - - qint64 oldPos = inFile.pos(); - while (!inFile.atEnd()) { - const QString line = inFile.readLine(); - if (line.trimmed().isEmpty()) { - continue; - } - - const QRegularExpressionMatch nameMatch = nameRegExp.match(line); - if (nameMatch.hasMatch()) { - inFile.seek(oldPos); // Revert file cursor position for the next Application parsing - return app; - } - - const QRegularExpressionMatch packageNameMatch = packageNameRegExp.match(line); - if (packageNameMatch.hasMatch() && packageNameMatch.lastCapturedIndex() > 0) { - app->m_packageName = packageNameMatch.captured(packageNameMatch.lastCapturedIndex()); - } - - oldPos = inFile.pos(); - } - - return app; -} - -QString WaydroidApplication::name() const -{ - return m_name; -} - -QString WaydroidApplication::packageName() const -{ - return m_packageName; -} diff --git a/components/waydroidintegrationplugin/waydroidapplication.h b/components/waydroidintegrationplugin/waydroidapplication.h deleted file mode 100644 index b100fe42..00000000 --- a/components/waydroidintegrationplugin/waydroidapplication.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Florian RICHER - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#pragma once - -#include -#include -#include - -class WaydroidApplication : public QObject, public std::enable_shared_from_this -{ - Q_OBJECT - Q_PROPERTY(QString name READ name CONSTANT) - Q_PROPERTY(QString packageName READ packageName CONSTANT) - -public: - typedef std::shared_ptr Ptr; - - WaydroidApplication(QObject *parent = nullptr); - - /** - * @brief Read from "waydroid app list" command output log through QTextStream. - * The QTextStream cursor must be set to the first line of the application. - * The first line begin with "Name:". - * - * @param inFile The QTextStream used to read line by line the Waydroid logs. - */ - static WaydroidApplication::Ptr fromWaydroidLog(QTextStream &inFile); - - QString name() const; - QString packageName() const; - -private: - QString m_name; - QString m_packageName; -}; diff --git a/components/waydroidintegrationplugin/waydroidapplicationdbusclient.cpp b/components/waydroidintegrationplugin/waydroidapplicationdbusclient.cpp new file mode 100644 index 00000000..c85ab079 --- /dev/null +++ b/components/waydroidintegrationplugin/waydroidapplicationdbusclient.cpp @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: 2025 Florian RICHER + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "waydroidapplicationdbusclient.h" + +#include + +using namespace Qt::StringLiterals; + +WaydroidApplicationDBusClient::WaydroidApplicationDBusClient(const QDBusObjectPath &objectPath, QObject *parent) + : QObject{parent} + , m_objectPath{objectPath} + , m_interface{new OrgKdePlasmashellWaydroidApplicationInterface{u"org.kde.plasmashell"_s, objectPath.path(), QDBusConnection::sessionBus(), this}} + , m_watcher{new QDBusServiceWatcher{u"org.kde.plasmashell"_s, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this}} +{ + // Check if the service is already running + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(u"org.kde.plasmashell"_s)) { + m_connected = true; + if (m_interface->isValid()) { + connectSignals(); + } + } + + connect(m_watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, [this](const QString &service, const QString &oldOwner, const QString &newOwner) { + if (service == u"org.kde.plasmashell"_s) { + if (newOwner.isEmpty()) { + // Service stopped + m_connected = false; + } else if (oldOwner.isEmpty()) { + // Service started + m_connected = true; + if (m_interface->isValid()) { + connectSignals(); + } + } + } + }); +} + +void WaydroidApplicationDBusClient::connectSignals() +{ + // Initialize properties + updateName(); + updatePackageName(); +} + +QString WaydroidApplicationDBusClient::name() const +{ + return m_name; +} + +QString WaydroidApplicationDBusClient::packageName() const +{ + return m_packageName; +} + +QDBusObjectPath WaydroidApplicationDBusClient::objectPath() const +{ + return m_objectPath; +} + +void WaydroidApplicationDBusClient::updateName() +{ + if (!m_connected) { + return; + } + + auto reply = m_interface->name(); + auto watcher = new QDBusPendingCallWatcher(reply, this); + + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) { + QDBusPendingReply reply = *watcher; + const auto name = reply.argumentAt<0>(); + + if (m_name != name) { + m_name = name; + Q_EMIT nameChanged(); + } + }); +} + +void WaydroidApplicationDBusClient::updatePackageName() +{ + if (!m_connected) { + return; + } + + auto reply = m_interface->name(); + auto watcher = new QDBusPendingCallWatcher(reply, this); + + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) { + QDBusPendingReply reply = *watcher; + const auto packageName = reply.argumentAt<0>(); + + if (m_packageName != packageName) { + m_packageName = packageName; + Q_EMIT packageNameChanged(); + } + }); +} \ No newline at end of file diff --git a/components/waydroidintegrationplugin/waydroidapplicationdbusclient.h b/components/waydroidintegrationplugin/waydroidapplicationdbusclient.h new file mode 100644 index 00000000..2ef2d3fd --- /dev/null +++ b/components/waydroidintegrationplugin/waydroidapplicationdbusclient.h @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2025 Florian RICHER + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include "plasmashellwaydroidapplicationinterface.h" + +#include +#include + +#include + +/** + * This class provides a DBus client interface for individual Waydroid application. + * It connects to WaydroidApplicationDBusObject instances via DBus. + * + * @author Florian RICHER + */ +class WaydroidApplicationDBusClient : public QObject +{ + Q_OBJECT + QML_UNCREATABLE("") + QML_ELEMENT + + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString packageName READ packageName NOTIFY packageNameChanged) + +public: + typedef std::shared_ptr Ptr; + + explicit WaydroidApplicationDBusClient(const QDBusObjectPath &objectPath, QObject *parent = nullptr); + + QString name() const; + QString packageName() const; + QDBusObjectPath objectPath() const; + +private Q_SLOTS: + void updateName(); + void updatePackageName(); + +Q_SIGNALS: + void nameChanged(); + void packageNameChanged(); + +private: + OrgKdePlasmashellWaydroidApplicationInterface *m_interface; + QDBusServiceWatcher *m_watcher; + + QString m_packageName; + QString m_name; + QDBusObjectPath m_objectPath; + bool m_connected{false}; + + void connectSignals(); +}; \ No newline at end of file diff --git a/components/waydroidintegrationplugin/waydroidapplicationdbusobject.cpp b/components/waydroidintegrationplugin/waydroidapplicationdbusobject.cpp new file mode 100644 index 00000000..aff9ad62 --- /dev/null +++ b/components/waydroidintegrationplugin/waydroidapplicationdbusobject.cpp @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2025 Florian RICHER + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "waydroidapplicationdbusobject.h" +#include "waydroidapplicationadaptor.h" +#include "waydroidintegrationplugin_debug.h" + +#include +#include +#include + +using namespace Qt::StringLiterals; + +static const QRegularExpression nameRegExp(u"^Name:\\s*(\\S+)"_s); +static const QRegularExpression packageNameRegExp(u"^packageName:\\s*(\\S+)"_s); + +WaydroidApplicationDBusObject::WaydroidApplicationDBusObject(QObject *parent) + : QObject{parent} +{ +} + +void WaydroidApplicationDBusObject::registerObject() +{ + if (!m_dbusInitialized) { + new WaydroidApplicationAdaptor{this}; + QString sanitizedPackageName = m_packageName; + sanitizedPackageName.replace(".", "_"); + const QString objectPath = u"/WaydroidApplication/%1"_s.arg(sanitizedPackageName); + QDBusConnection::sessionBus().registerObject(objectPath, this); + m_objectPath = QDBusObjectPath(objectPath); + } +} + +void WaydroidApplicationDBusObject::unregisterObject() +{ + if (m_dbusInitialized) { + QDBusConnection::sessionBus().unregisterObject(m_objectPath.path()); + m_dbusInitialized = false; + } +} + +QDBusObjectPath WaydroidApplicationDBusObject::objectPath() const +{ + return m_objectPath; +} + +WaydroidApplicationDBusObject::Ptr WaydroidApplicationDBusObject::parseApplicationFromWaydroidLog(QTextStream &inFile) +{ + const QString line = inFile.readLine(); + const QRegularExpressionMatch nameMatch = nameRegExp.match(line); + + if (!nameMatch.hasMatch() || nameMatch.lastCapturedIndex() == 0) { + return nullptr; + } + + auto app = std::make_shared(); + app->m_name = nameMatch.captured(nameMatch.lastCapturedIndex()); + + qint64 oldPos = inFile.pos(); + while (!inFile.atEnd()) { + const QString line = inFile.readLine(); + if (line.trimmed().isEmpty()) { + continue; + } + + const QRegularExpressionMatch nameMatch = nameRegExp.match(line); + if (nameMatch.hasMatch()) { + inFile.seek(oldPos); // Revert file cursor position for the next Application parsing + return app; + } + + const QRegularExpressionMatch packageNameMatch = packageNameRegExp.match(line); + if (packageNameMatch.hasMatch() && packageNameMatch.lastCapturedIndex() > 0) { + app->m_packageName = packageNameMatch.captured(packageNameMatch.lastCapturedIndex()); + } + + oldPos = inFile.pos(); + } + + return app; +} + +QList WaydroidApplicationDBusObject::parseApplicationsFromWaydroidLog(QTextStream &inFile) +{ + QList applications; + while (!inFile.atEnd()) { + const auto app = parseApplicationFromWaydroidLog(inFile); + if (app == nullptr) { + qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to fetch the application: Maybe wrong QTextStream cursor position."; + break; + } + + qCDebug(WAYDROIDINTEGRATIONPLUGIN) << "Waydroid application found: " << app.get()->name() << " (" << app.get()->packageName() << ")"; + applications.append(app); + } + return applications; +} + +QString WaydroidApplicationDBusObject::name() const +{ + return m_name; +} + +QString WaydroidApplicationDBusObject::packageName() const +{ + return m_packageName; +} \ No newline at end of file diff --git a/components/waydroidintegrationplugin/waydroidapplicationdbusobject.h b/components/waydroidintegrationplugin/waydroidapplicationdbusobject.h new file mode 100644 index 00000000..7955dfdb --- /dev/null +++ b/components/waydroidintegrationplugin/waydroidapplicationdbusobject.h @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2025 Florian RICHER + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include +#include + +#include + +/** + * This class provides a DBus interface for individual Waydroid applications. + * Each application gets its own DBus object registered at a unique path. + * + * @author Florian RICHER + */ +class WaydroidApplicationDBusObject : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_CLASSINFO("D-Bus Interface", "org.kde.plasmashell.WaydroidApplication") + +public: + typedef std::shared_ptr Ptr; + + explicit WaydroidApplicationDBusObject(QObject *parent = nullptr); + + void registerObject(); + void unregisterObject(); + [[nodiscard]] QDBusObjectPath objectPath() const; + + /** + * @brief Read one application from "waydroid app list" command output log through QTextStream. + * The QTextStream cursor must be set to the first line of the application. + * The first line begin with "Name:". + * + * @param inFile The QTextStream used to read line by line the Waydroid logs. + * @return One parsed application DBus object, or std::nullopt if parsing failed + */ + static Ptr parseApplicationFromWaydroidLog(QTextStream &inFile); + + /** + * @brief Read all applications from "waydroid app list" command output log through QTextStream. + * + * @param inFile The QTextStream used to read line by line the Waydroid logs. + * @return All parsed application DBus objects + */ + static QList parseApplicationsFromWaydroidLog(QTextStream &inFile); + +public Q_SLOTS: + Q_SCRIPTABLE QString name() const; + Q_SCRIPTABLE QString packageName() const; + +private: + bool m_dbusInitialized{false}; + QDBusObjectPath m_objectPath; + QString m_name; + QString m_packageName; +}; \ No newline at end of file diff --git a/components/waydroidintegrationplugin/waydroidapplicationlistmodel.cpp b/components/waydroidintegrationplugin/waydroidapplicationlistmodel.cpp index 3eaf8b3b..09acae2a 100644 --- a/components/waydroidintegrationplugin/waydroidapplicationlistmodel.cpp +++ b/components/waydroidintegrationplugin/waydroidapplicationlistmodel.cpp @@ -5,123 +5,79 @@ */ #include "waydroidapplicationlistmodel.h" -#include "waydroidintegrationplugin_debug.h" -#include "waydroidshared.h" - -#include -#include -#include +#include "waydroidapplicationdbusclient.h" +#include "waydroiddbusclient.h" #include using namespace Qt::StringLiterals; using namespace std::chrono_literals; -WaydroidApplicationListModel::WaydroidApplicationListModel(WaydroidState *parent) +WaydroidApplicationListModel::WaydroidApplicationListModel(WaydroidDBusClient *parent) : QAbstractListModel{parent} - , m_waydroidState{parent} - , m_refreshTimer{new QTimer{this}} + , m_waydroidDBusClient{parent} { - // Waydroid does not return all installed applications immediately, so we need to refresh regularly. - m_refreshTimer->setInterval(1s); - m_refreshTimer->setSingleShot(false); - m_refreshTimer->start(); - - connect(m_refreshTimer, &QTimer::timeout, this, &WaydroidApplicationListModel::refreshApplications); - connect(parent, &WaydroidState::sessionStatusChanged, this, &WaydroidApplicationListModel::refreshApplications); } WaydroidApplicationListModel::~WaydroidApplicationListModel() = default; -void WaydroidApplicationListModel::loadApplications(const QList applications) +void WaydroidApplicationListModel::initializeApplications(const QList &applicationObjectPaths) { - if (m_waydroidState->sessionStatus() != WaydroidState::SessionRunning) { + if (!m_applications.isEmpty()) { return; } - qCDebug(WAYDROIDINTEGRATIONPLUGIN) << "Reload waydroid apps"; - - QMap appIdMap; // - for (int i = 0; i < m_applications.size(); ++i) { - const auto &application = m_applications[i]; - appIdMap.insert(application->packageName(), i); - } - - QList toInsert; - - for (const WaydroidApplication::Ptr &application : applications) { - auto it = appIdMap.find(application->packageName()); - if (it != appIdMap.end()) { - // Application already in m_applications - appIdMap.erase(it); - } else { - // Application needs to be inserted into m_applications - toInsert.append(std::move(application)); - } - } - - QList toRemove; - for (int index : appIdMap.values()) { - toRemove.append(index); - } - - std::sort(toRemove.begin(), toRemove.end()); - - // Remove indices first, from end to start to avoid indices changing - for (int i = toRemove.size() - 1; i >= 0; --i) { - int ind = toRemove[i]; - - beginRemoveRows({}, ind, ind); - m_applications.removeAt(ind); - endRemoveRows(); - } - - // Append new elements - for (const WaydroidApplication::Ptr &application : toInsert) { - beginInsertRows({}, m_applications.size(), m_applications.size()); - m_applications.append(application); - endInsertRows(); + beginResetModel(); + for (const QDBusObjectPath &applicationObjectPath : applicationObjectPaths) { + auto client = std::make_shared(applicationObjectPath, this); + m_applications.append(client); } + endResetModel(); } -void WaydroidApplicationListModel::refreshApplications() +void WaydroidApplicationListModel::addApplication(const QDBusObjectPath &objectPath) { - QList applications; - - QStringList arguments = {u"app"_s, u"list"_s}; - - QProcess *process = new QProcess(m_waydroidState); - process->start(WAYDROID_COMMAND, arguments); - - connect(process, &QProcess::finished, this, [this, process](int exitCode, QProcess::ExitStatus exitStatus) { - if (exitCode != 0 || exitStatus == QProcess::ExitStatus::CrashExit) { - qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to run waydroid app list command: " << process->readAllStandardError(); - return; - } - - const QByteArray data = process->readAllStandardOutput(); - if (data.isEmpty()) { - qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Empty data: " << process->readAllStandardError(); - return; - } - - qCDebug(WAYDROIDINTEGRATIONPLUGIN) << "Waydroid output: " << data; - QTextStream output = QTextStream(data); - - QList applications; - while (!output.atEnd()) { - const WaydroidApplication::Ptr app = WaydroidApplication::fromWaydroidLog(output); - if (app == nullptr) { - qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to fetch the application: Maybe wrong QTextStream cursor position."; - break; - } - - qCDebug(WAYDROIDINTEGRATIONPLUGIN) << "Waydroid application found: " << app.get()->name() << " (" << app.get()->packageName() << ")"; - applications.append(app); - } - - loadApplications(applications); + beginInsertRows({}, m_applications.size(), m_applications.size()); + auto client = std::make_shared(objectPath, this); + connect(client.get(), &WaydroidApplicationDBusClient::nameChanged, this, [this, objectPath] { + updateApplication(objectPath, {Qt::DisplayRole, DelegateRole, NameRole}); }); + connect(client.get(), &WaydroidApplicationDBusClient::packageNameChanged, this, [this, objectPath] { + updateApplication(objectPath, {Qt::DisplayRole, DelegateRole, IdRole}); + }); + m_applications.append(client); + endInsertRows(); +} + +void WaydroidApplicationListModel::updateApplication(const QDBusObjectPath &objectPath, const QList &roles) +{ + const auto it = std::ranges::find_if(m_applications, [objectPath](auto app) { + return app->objectPath() == objectPath; + }); + + if (it == m_applications.end()) { + return; + } + + int ind = std::distance(m_applications.begin(), it); + QModelIndex index = createIndex(ind, 0); + Q_EMIT dataChanged(index, index, roles); +} + +void WaydroidApplicationListModel::removeApplication(const QDBusObjectPath &objectPath) +{ + const auto it = std::ranges::find_if(m_applications, [objectPath](auto app) { + return app->objectPath() == objectPath; + }); + + if (it == m_applications.end()) { + return; + } + + int ind = std::distance(m_applications.begin(), it); + beginRemoveRows({}, ind, ind); + m_applications.erase(it); + endRemoveRows(); } QHash WaydroidApplicationListModel::roleNames() const @@ -135,7 +91,7 @@ QVariant WaydroidApplicationListModel::data(const QModelIndex &index, int role) return QVariant(); } - WaydroidApplication::Ptr app = m_applications.at(index.row()); + WaydroidApplicationDBusClient::Ptr app = m_applications.at(index.row()); switch (role) { case Qt::DisplayRole: @@ -158,43 +114,3 @@ int WaydroidApplicationListModel::rowCount(const QModelIndex &parent) const return m_applications.count(); } - -void WaydroidApplicationListModel::installApk(const QString apkFile) -{ - const QStringList arguments{u"app"_s, u"install"_s, apkFile}; - - QProcess *process = new QProcess(this); - process->start(WAYDROID_COMMAND, arguments); - - connect(process, &QProcess::finished, this, [this, apkFile, process](int exitCode, QProcess::ExitStatus exitStatus) { - if (exitCode == 0 && exitStatus == QProcess::NormalExit) { - Q_EMIT actionFinished(i18n("Application has been installed")); - } else { - Q_EMIT errorOccurred(i18n("Installation Failed")); - qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Error occurred during installation of " << apkFile << ": " << process->readAllStandardError(); - } - }); -} - -void WaydroidApplicationListModel::deleteApplication(const QString appId) -{ - const QStringList arguments{u"app"_s, u"remove"_s, appId}; - - QProcess *process = new QProcess(this); - process->start(WAYDROID_COMMAND, arguments); - - connect(process, &QProcess::finished, this, [this, appId, process](int exitCode, QProcess::ExitStatus exitStatus) { - Q_UNUSED(exitCode); - Q_UNUSED(exitStatus); - - const QByteArray errorLog = process->readAllStandardError(); - - // "waydroid app remove" send log on stderr but keep exitCode to 0 - if (errorLog.isEmpty()) { - Q_EMIT actionFinished(i18n("Application has been deleted")); - } else { - Q_EMIT errorOccurred(i18n("Application uninstall failed")); - qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Error occurred during uninstallation of " << appId << ": " << errorLog; - } - }); -} \ No newline at end of file diff --git a/components/waydroidintegrationplugin/waydroidapplicationlistmodel.h b/components/waydroidintegrationplugin/waydroidapplicationlistmodel.h index 57f037e3..83716e00 100644 --- a/components/waydroidintegrationplugin/waydroidapplicationlistmodel.h +++ b/components/waydroidintegrationplugin/waydroidapplicationlistmodel.h @@ -6,14 +6,13 @@ #pragma once -#include "waydroidapplication.h" -#include "waydroidstate.h" +#include "waydroidapplicationdbusclient.h" #include #include #include -class WaydroidState; +class WaydroidDBusClient; class WaydroidApplicationListModel : public QAbstractListModel { @@ -26,25 +25,23 @@ public: IdRole }; - WaydroidApplicationListModel(WaydroidState *parent = nullptr); + explicit WaydroidApplicationListModel(WaydroidDBusClient *parent = nullptr); ~WaydroidApplicationListModel() override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; - Q_INVOKABLE void installApk(const QString apkFile); - Q_INVOKABLE void deleteApplication(const QString appId); + void initializeApplications(const QList &applicationObjectPaths); -Q_SIGNALS: - void actionFinished(const QString message); - void errorOccurred(const QString message); +public Q_SLOTS: + void addApplication(const QDBusObjectPath &objectPath); + void removeApplication(const QDBusObjectPath &objectPath); private: - WaydroidState *m_waydroidState{nullptr}; - QList m_applications; + WaydroidDBusClient *m_waydroidDBusClient{nullptr}; + QList m_applications; QTimer *m_refreshTimer{nullptr}; - void loadApplications(const QList applications); - void refreshApplications(); + void updateApplication(const QDBusObjectPath &objectPath, const QList &roles); }; \ No newline at end of file diff --git a/components/waydroidintegrationplugin/waydroiddbusclient.cpp b/components/waydroidintegrationplugin/waydroiddbusclient.cpp new file mode 100644 index 00000000..66a7233d --- /dev/null +++ b/components/waydroidintegrationplugin/waydroiddbusclient.cpp @@ -0,0 +1,404 @@ +/* + * SPDX-FileCopyrightText: 2025 Florian RICHER + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "waydroiddbusclient.h" + +#include +#include +#include + +using namespace Qt::StringLiterals; + +WaydroidDBusClient::WaydroidDBusClient(QObject *parent) + : QObject{parent} + , m_interface{new OrgKdePlasmashellWaydroidInterface{u"org.kde.plasmashell"_s, u"/Waydroid"_s, QDBusConnection::sessionBus(), this}} + , m_watcher{new QDBusServiceWatcher{u"org.kde.plasmashell"_s, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this}} + , m_applicationListModel{new WaydroidApplicationListModel{this}} +{ + // Check if the service is already running + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(u"org.kde.plasmashell"_s)) { + m_connected = true; + if (m_interface->isValid()) { + connectSignals(); + } + } + + connect(m_watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, [this](const QString &service, const QString &oldOwner, const QString &newOwner) { + if (service == u"org.kde.plasmashell"_s) { + if (newOwner.isEmpty()) { + // Service stopped + m_connected = false; + } else if (oldOwner.isEmpty()) { + // Service started + m_connected = true; + if (m_interface->isValid()) { + connectSignals(); + } + } + } + }); +} + +void WaydroidDBusClient::connectSignals() +{ + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::statusChanged, this, &WaydroidDBusClient::updateStatus); + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::downloadStatusChanged, this, [this](double downloaded, double total, double speed) { + Q_EMIT downloadStatusChanged(downloaded, total, speed); + }); + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::sessionStatusChanged, this, &WaydroidDBusClient::updateSessionStatus); + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::systemTypeChanged, this, &WaydroidDBusClient::updateSystemType); + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::ipAddressChanged, this, &WaydroidDBusClient::updateIpAddress); + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::androidIdChanged, this, &WaydroidDBusClient::updateAndroidId); + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::multiWindowsChanged, this, &WaydroidDBusClient::updateMultiWindows); + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::suspendChanged, this, &WaydroidDBusClient::updateSuspend); + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::ueventChanged, this, &WaydroidDBusClient::updateUevent); + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::actionFinished, this, [this](const QString message) { + Q_EMIT actionFinished(message); + }); + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::actionFailed, this, [this](const QString message) { + Q_EMIT actionFailed(message); + }); + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::errorOccurred, this, [this](const QString title, const QString message) { + Q_EMIT errorOccurred(title, message); + }); + + initializeApplicationListModel(); + updateStatus(); + updateSessionStatus(); + updateSystemType(); + updateIpAddress(); + updateAndroidId(); + updateMultiWindows(); + updateSuspend(); + updateUevent(); +} + +void WaydroidDBusClient::initializeApplicationListModel() +{ + auto reply = m_interface->applications(); + auto watcher = new QDBusPendingCallWatcher(reply, this); + + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) { + QDBusPendingReply> reply = *watcher; + const auto applications = reply.argumentAt<0>(); + + m_applicationListModel->initializeApplications(applications); + + // Connect applicationListModel signals only when applications is synced + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::applicationAdded, m_applicationListModel, &WaydroidApplicationListModel::addApplication); + connect(m_interface, &OrgKdePlasmashellWaydroidInterface::applicationRemoved, m_applicationListModel, &WaydroidApplicationListModel::removeApplication); + }); +} + +WaydroidDBusClient::Status WaydroidDBusClient::status() const +{ + return m_status; +} + +WaydroidDBusClient::SessionStatus WaydroidDBusClient::sessionStatus() const +{ + return m_sessionStatus; +} + +WaydroidDBusClient::SystemType WaydroidDBusClient::systemType() const +{ + return m_systemType; +} + +QString WaydroidDBusClient::ipAddress() const +{ + return m_ipAddress; +} + +QString WaydroidDBusClient::androidId() const +{ + return m_androidId; +} + +WaydroidApplicationListModel *WaydroidDBusClient::applicationListModel() const +{ + return m_applicationListModel; +} + +QCoro::Task WaydroidDBusClient::setMultiWindowsTask(const bool multiWindows) +{ + auto pendingReply = m_interface->setMultiWindows(multiWindows); + co_await pendingReply; +} + +QCoro::QmlTask WaydroidDBusClient::setMultiWindows(const bool multiWindows) +{ + return setMultiWindowsTask(multiWindows); +} + +bool WaydroidDBusClient::multiWindows() const +{ + return m_multiWindows; +} + +QCoro::Task WaydroidDBusClient::setSuspendTask(const bool suspend) +{ + auto pendingReply = m_interface->setSuspend(suspend); + co_await pendingReply; +} + +QCoro::QmlTask WaydroidDBusClient::setSuspend(const bool suspend) +{ + return setSuspendTask(suspend); +} + +bool WaydroidDBusClient::suspend() const +{ + return m_suspend; +} + +QCoro::Task WaydroidDBusClient::setUeventTask(const bool uevent) +{ + auto pendingReply = m_interface->setUevent(uevent); + co_await pendingReply; +} + +QCoro::QmlTask WaydroidDBusClient::setUevent(const bool multiWindows) +{ + return setUeventTask(multiWindows); +} + +QCoro::Task WaydroidDBusClient::refreshSessionInfoTask() +{ + auto pendingReply = m_interface->refreshSessionInfo(); + co_await pendingReply; +} + +QCoro::QmlTask WaydroidDBusClient::refreshSessionInfo() +{ + return refreshSessionInfoTask(); +} + +QCoro::Task WaydroidDBusClient::refreshAndroidIdTask() +{ + auto pendingReply = m_interface->refreshAndroidId(); + co_await pendingReply; +} + +QCoro::QmlTask WaydroidDBusClient::refreshAndroidId() +{ + return refreshAndroidIdTask(); +} + +QCoro::Task WaydroidDBusClient::refreshApplicationsTask() +{ + auto pendingReply = m_interface->refreshApplications(); + co_await pendingReply; +} + +QCoro::QmlTask WaydroidDBusClient::refreshApplications() +{ + return refreshApplicationsTask(); +} + +bool WaydroidDBusClient::uevent() const +{ + return m_uevent; +} + +QCoro::Task WaydroidDBusClient::initializeTask(const SystemType systemType, const RomType romType, const bool forced) +{ + auto pendingReply = m_interface->initialize(systemType, romType, forced); + co_await pendingReply; +} + +QCoro::QmlTask WaydroidDBusClient::initialize(const SystemType systemType, const RomType romType, const bool forced) +{ + return initializeTask(systemType, romType, forced); +} + +QCoro::Task WaydroidDBusClient::startSessionTask() +{ + auto pendingReply = m_interface->startSession(); + co_await pendingReply; +} + +QCoro::QmlTask WaydroidDBusClient::startSession() +{ + return startSessionTask(); +} + +QCoro::Task WaydroidDBusClient::stopSessionTask() +{ + auto pendingReply = m_interface->stopSession(); + co_await pendingReply; +} + +QCoro::QmlTask WaydroidDBusClient::stopSession() +{ + return stopSessionTask(); +} + +QCoro::Task WaydroidDBusClient::resetWaydroidTask() +{ + auto pendingReply = m_interface->resetWaydroid(); + co_await pendingReply; +} + +QCoro::QmlTask WaydroidDBusClient::resetWaydroid() +{ + return resetWaydroidTask(); +} + +QCoro::Task WaydroidDBusClient::installApkTask(const QString apkFile) +{ + auto pendingReply = m_interface->installApk(apkFile); + co_await pendingReply; +} + +QCoro::QmlTask WaydroidDBusClient::installApk(const QString apkFile) +{ + return installApkTask(apkFile); +} + +QCoro::Task WaydroidDBusClient::deleteApplicationTask(const QString appId) +{ + auto pendingReply = m_interface->deleteApplication(appId); + co_await pendingReply; +} + +QCoro::QmlTask WaydroidDBusClient::deleteApplication(const QString appId) +{ + return deleteApplicationTask(appId); +} + +void WaydroidDBusClient::updateStatus() +{ + auto reply = m_interface->status(); + auto watcher = new QDBusPendingCallWatcher(reply, this); + + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) { + QDBusPendingReply reply = *watcher; + const auto status = static_cast(reply.argumentAt<0>()); + + if (m_status != status) { + m_status = status; + Q_EMIT statusChanged(); + } + }); +} + +void WaydroidDBusClient::updateSessionStatus() +{ + auto reply = m_interface->sessionStatus(); + auto watcher = new QDBusPendingCallWatcher(reply, this); + + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) { + QDBusPendingReply reply = *watcher; + const auto sessionStatus = static_cast(reply.argumentAt<0>()); + + if (m_sessionStatus != sessionStatus) { + m_sessionStatus = sessionStatus; + Q_EMIT sessionStatusChanged(); + } + }); +} + +void WaydroidDBusClient::updateSystemType() +{ + auto reply = m_interface->systemType(); + auto watcher = new QDBusPendingCallWatcher(reply, this); + + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) { + QDBusPendingReply reply = *watcher; + const auto systemType = static_cast(reply.argumentAt<0>()); + + if (m_systemType != systemType) { + m_systemType = systemType; + Q_EMIT systemTypeChanged(); + } + }); +} + +void WaydroidDBusClient::updateIpAddress() +{ + auto reply = m_interface->ipAddress(); + auto watcher = new QDBusPendingCallWatcher(reply, this); + + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) { + QDBusPendingReply reply = *watcher; + const auto ipAddress = reply.argumentAt<0>(); + + if (m_ipAddress != ipAddress) { + m_ipAddress = ipAddress; + Q_EMIT ipAddressChanged(); + } + }); +} + +void WaydroidDBusClient::updateAndroidId() +{ + auto reply = m_interface->androidId(); + auto watcher = new QDBusPendingCallWatcher(reply, this); + + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) { + QDBusPendingReply reply = *watcher; + const auto androidId = reply.argumentAt<0>(); + + if (m_androidId != androidId) { + m_androidId = androidId; + Q_EMIT androidIdChanged(); + } + }); +} + +void WaydroidDBusClient::updateMultiWindows() +{ + auto reply = m_interface->multiWindows(); + auto watcher = new QDBusPendingCallWatcher(reply, this); + + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) { + QDBusPendingReply reply = *watcher; + const auto multiWindows = reply.argumentAt<0>(); + + if (m_multiWindows != multiWindows) { + m_multiWindows = multiWindows; + Q_EMIT multiWindowsChanged(); + } + }); +} + +void WaydroidDBusClient::updateSuspend() +{ + auto reply = m_interface->suspend(); + auto watcher = new QDBusPendingCallWatcher(reply, this); + + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) { + QDBusPendingReply reply = *watcher; + const auto suspend = reply.argumentAt<0>(); + + if (m_suspend != suspend) { + m_suspend = suspend; + Q_EMIT suspendChanged(); + } + }); +} + +void WaydroidDBusClient::updateUevent() +{ + auto reply = m_interface->uevent(); + auto watcher = new QDBusPendingCallWatcher(reply, this); + + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) { + QDBusPendingReply reply = *watcher; + const auto uevent = reply.argumentAt<0>(); + + if (m_uevent != uevent) { + m_uevent = uevent; + Q_EMIT ueventChanged(); + } + }); +} + +void WaydroidDBusClient::copyToClipboard(const QString text) +{ + qGuiApp->clipboard()->setText(text); +} \ No newline at end of file diff --git a/components/waydroidintegrationplugin/waydroiddbusclient.h b/components/waydroidintegrationplugin/waydroiddbusclient.h new file mode 100644 index 00000000..1b707936 --- /dev/null +++ b/components/waydroidintegrationplugin/waydroiddbusclient.h @@ -0,0 +1,173 @@ +/* + * SPDX-FileCopyrightText: 2025 Florian RICHER + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include "plasmashellwaydroidinterface.h" +#include "waydroidapplicationlistmodel.h" +#include "waydroiddbusobject.h" + +#include +#include +#include +#include +#include + +#include + +class WaydroidDBusClient : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(Status status READ status NOTIFY statusChanged) + Q_PROPERTY(SessionStatus sessionStatus READ sessionStatus NOTIFY sessionStatusChanged) + Q_PROPERTY(SystemType systemType READ systemType NOTIFY systemTypeChanged) + Q_PROPERTY(QString ipAddress READ ipAddress NOTIFY ipAddressChanged) + Q_PROPERTY(QString androidId READ androidId NOTIFY androidIdChanged) + Q_PROPERTY(bool multiWindows READ multiWindows WRITE setMultiWindows NOTIFY multiWindowsChanged) + Q_PROPERTY(bool suspend READ suspend WRITE setSuspend NOTIFY suspendChanged) + Q_PROPERTY(bool uevent READ uevent WRITE setUevent NOTIFY ueventChanged) + Q_PROPERTY(WaydroidApplicationListModel *applicationListModel READ applicationListModel CONSTANT) + +public: + explicit WaydroidDBusClient(QObject *parent = nullptr); + + /** + * @enum Status + * @brief Defines the possible installation statuses of the Waydroid service. + */ + enum Status { + NotSupported = WaydroidDBusObject::NotSupported, + NotInitialized = WaydroidDBusObject::NotInitialized, + Initializing = WaydroidDBusObject::Initializing, + Initialized = WaydroidDBusObject::Initialized, + Resetting = WaydroidDBusObject::Resetting, + }; + Q_ENUM(Status) + + /** + * @enum SessionStatus + * @brief Defines the possible states of a Waydroid session. + */ + enum SessionStatus { + SessionStopped = WaydroidDBusObject::SessionStopped, + SessionStarting = WaydroidDBusObject::SessionStarting, + SessionRunning = WaydroidDBusObject::SessionRunning, + }; + Q_ENUM(SessionStatus) + + /** + * @enum SystemType + * @brief Defines the types of Android systems supported by Waydroid. + */ + enum SystemType { + Vanilla = WaydroidDBusObject::Vanilla, ///< Vanilla Android system. + Foss = WaydroidDBusObject::Foss, ///< Free and Open Source Software variant. + Gapps = WaydroidDBusObject::Gapps, ///< Variant with Google Apps included. + UnknownSystemType = WaydroidDBusObject::UnknownSystemType + }; + Q_ENUM(SystemType) + + /** + * @enum RomType + * @brief Defines the types of ROMs supported by Waydroid. + * + * @todo Add OTA ROM with custom system url and vendor url + */ + enum RomType { + Lineage = WaydroidDBusObject::Lineage, ///< LineageOS ROM. + Bliss = WaydroidDBusObject::Bliss ///< Bliss ROM. + }; + Q_ENUM(RomType) + + [[nodiscard]] Status status() const; + [[nodiscard]] SessionStatus sessionStatus() const; + [[nodiscard]] SystemType systemType() const; + [[nodiscard]] QString ipAddress() const; + [[nodiscard]] QString androidId() const; + [[nodiscard]] WaydroidApplicationListModel *applicationListModel() const; + + [[nodiscard]] bool multiWindows() const; + QCoro::QmlTask setMultiWindows(const bool multiWindows); + [[nodiscard]] bool suspend() const; + QCoro::QmlTask setSuspend(const bool suspend); + [[nodiscard]] bool uevent() const; + QCoro::QmlTask setUevent(const bool uevent); + + Q_INVOKABLE QCoro::QmlTask initialize(const SystemType systemType, const RomType romType, const bool forced = false); + Q_INVOKABLE QCoro::QmlTask startSession(); + Q_INVOKABLE QCoro::QmlTask stopSession(); + Q_INVOKABLE QCoro::QmlTask resetWaydroid(); + Q_INVOKABLE QCoro::QmlTask installApk(const QString apkFile); + Q_INVOKABLE QCoro::QmlTask deleteApplication(const QString appId); + Q_INVOKABLE QCoro::QmlTask refreshSessionInfo(); + Q_INVOKABLE QCoro::QmlTask refreshAndroidId(); + Q_INVOKABLE QCoro::QmlTask refreshApplications(); + + Q_INVOKABLE void copyToClipboard(const QString text); + +Q_SIGNALS: + void statusChanged(); + // download and total is in MB and speed in Kbps + void downloadStatusChanged(double downloaded, double total, double speed); + void sessionStatusChanged(); + void systemTypeChanged(); + void ipAddressChanged(); + void androidIdChanged(); + void multiWindowsChanged(); + void suspendChanged(); + void ueventChanged(); + + void actionFinished(const QString message); + void actionFailed(const QString message); + void errorOccurred(const QString title, const QString message); + +private Q_SLOTS: + void updateStatus(); + void updateSessionStatus(); + void updateSystemType(); + void updateIpAddress(); + void updateAndroidId(); + void updateMultiWindows(); + void updateSuspend(); + void updateUevent(); + +private: + OrgKdePlasmashellWaydroidInterface *m_interface; + QDBusServiceWatcher *m_watcher; + + Status m_status{NotInitialized}; + SessionStatus m_sessionStatus{SessionStopped}; + SystemType m_systemType{UnknownSystemType}; + QString m_ipAddress{""}; + QString m_androidId{""}; + WaydroidApplicationListModel *m_applicationListModel{nullptr}; + + // Waydroid props. See https://docs.waydro.id/usage/waydroid-prop-options + bool m_multiWindows{false}; + bool m_suspend{false}; + bool m_uevent{false}; + + bool m_connected{false}; + + void connectSignals(); + void initializeApplicationListModel(); + + QCoro::Task initializeTask(const SystemType systemType, const RomType romType, const bool forced = false); + QCoro::Task startSessionTask(); + QCoro::Task stopSessionTask(); + QCoro::Task resetWaydroidTask(); + QCoro::Task installApkTask(const QString apkFile); + QCoro::Task deleteApplicationTask(const QString appId); + QCoro::Task setMultiWindowsTask(const bool multiWindows); + QCoro::Task setSuspendTask(const bool suspend); + QCoro::Task setUeventTask(const bool uevent); + QCoro::Task refreshSessionInfoTask(); + QCoro::Task refreshAndroidIdTask(); + QCoro::Task refreshApplicationsTask(); +}; \ No newline at end of file diff --git a/components/waydroidintegrationplugin/waydroidstate.cpp b/components/waydroidintegrationplugin/waydroiddbusobject.cpp similarity index 54% rename from components/waydroidintegrationplugin/waydroidstate.cpp rename to components/waydroidintegrationplugin/waydroiddbusobject.cpp index 7425e79a..5c41216e 100644 --- a/components/waydroidintegrationplugin/waydroidstate.cpp +++ b/components/waydroidintegrationplugin/waydroiddbusobject.cpp @@ -4,22 +4,20 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -#include "waydroidstate.h" +#include "waydroiddbusobject.h" +#include "waydroidadaptor.h" +#include "waydroidapplicationdbusobject.h" #include "waydroidintegrationplugin_debug.h" #include "waydroidshared.h" -#include -#include -#include +#include #include -#include +#include #include #include #include #include -#include -#include #include #include #include @@ -36,27 +34,330 @@ static const QRegularExpression sessionRegExp(u"Session:\\s*(\\w+)"_s); static const QRegularExpression ipAdressRegExp(u"IP address:\\s*(\\d+\\.\\d+\\.\\d+\\.\\d+)"_s); static const QRegularExpression systemOtaRegExp(u"system_ota\\s*=\\s*(\\S+)"_s); -WaydroidState::WaydroidState(QObject *parent) +WaydroidDBusObject::WaydroidDBusObject(QObject *parent) : QObject{parent} - , m_applicationListModel{new WaydroidApplicationListModel{this}} { - // Connect it-self to auto-refresh when required status has changed - connect(this, &WaydroidState::statusChanged, this, &WaydroidState::refreshSessionInfo); - connect(this, &WaydroidState::statusChanged, this, &WaydroidState::refreshInstallationInfo); - connect(this, &WaydroidState::sessionStatusChanged, this, &WaydroidState::refreshPropsInfo); - - refreshSupportsInfo(); } -void WaydroidState::refreshSupportsInfo() +void WaydroidDBusObject::registerObject() { - const QStringList arguments{u"-h"_s}; + if (!m_dbusInitialized) { + new WaydroidAdaptor{this}; + QDBusConnection::sessionBus().registerObject(u"/Waydroid"_s, this); + m_dbusInitialized = true; + + // Connect it-self to auto-refresh when required status has changed + connect(this, &WaydroidDBusObject::statusChanged, this, &WaydroidDBusObject::refreshSessionInfo); + connect(this, &WaydroidDBusObject::statusChanged, this, &WaydroidDBusObject::refreshInstallationInfo); + connect(this, &WaydroidDBusObject::sessionStatusChanged, this, &WaydroidDBusObject::refreshPropsInfo); + connect(this, &WaydroidDBusObject::sessionStatusChanged, this, &WaydroidDBusObject::refreshApplications); + + refreshSupportsInfo(); + } +} + +void WaydroidDBusObject::initialize(const int systemType, const int romType, const bool forced) +{ + if (m_status == Initializing) { + return; + } + + m_status = Initializing; + Q_EMIT statusChanged(); + + QString systemTypeArg; + switch (systemType) { + case Vanilla: + systemTypeArg = "VANILLA"; + break; + case Foss: + systemTypeArg = "FOSS"; + break; + case Gapps: + systemTypeArg = "GAPPS"; + break; + default: + systemTypeArg = "VANILLA"; + break; + } + + QString romTypeArg; + switch (romType) { + case Lineage: + romTypeArg = "lineage"; + break; + case Bliss: + romTypeArg = "bliss"; + break; + } + + const QVariantMap args = {{u"systemType"_s, systemTypeArg}, {u"romType"_s, romTypeArg}, {u"forced"_s, forced}}; + + KAuth::Action writeAction(u"org.kde.plasma.mobileshell.waydroidhelper.initialize"_s); + writeAction.setHelperId(u"org.kde.plasma.mobileshell.waydroidhelper"_s); + writeAction.setArguments(args); + writeAction.setTimeout(3600000); // HACK: 1 hour to wait installation + + KAuth::ExecuteJob *job = writeAction.execute(); + job->start(); + + connect(job, &KAuth::ExecuteJob::newData, this, [this](const QVariantMap &data) { + QString log = data.value("log", "").toString(); + float downloaded = data.value("downloaded", 0.0).toFloat(); + float total = data.value("total", 0.0).toFloat(); + float speed = data.value("speed", 0.0).toFloat(); + + qCDebug(WAYDROIDINTEGRATIONPLUGIN) << "log: " << log; + Q_EMIT downloadStatusChanged(downloaded, total, speed); + }); + + connect(job, &KAuth::ExecuteJob::finished, this, [this](KJob *job, auto) { + if (job->error() == 0) { + m_status = Initialized; + } else { + Q_EMIT errorOccurred(i18n("Failed to initialize Waydroid."), job->errorString()); + + m_status = NotInitialized; + qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "KAuth returned an error code:" << job->error() << " message: " << job->errorString(); + } + + Q_EMIT statusChanged(); + }); +} + +void WaydroidDBusObject::startSession() +{ + if (m_sessionStatus == SessionStarting || m_sessionStatus == SessionRunning) { + return; + } + + m_sessionStatus = SessionStarting; + Q_EMIT sessionStatusChanged(); + + const QStringList arguments{u"session"_s, u"start"_s}; + + auto *process = new QProcess(this); + process->start(WAYDROID_COMMAND, arguments); + + connect(process, &QProcess::finished, this, [this, process](int exitCode, QProcess::ExitStatus exitStatus) { + Q_UNUSED(exitStatus); + + if (exitCode == 0) { + return; + } + + m_sessionStatus = SessionStopped; + Q_EMIT sessionStatusChanged(); + + QByteArray errorData = process->readAllStandardError(); + QString errorString = QString::fromUtf8(errorData); + + Q_EMIT errorOccurred(i18n("Failed to start the Waydroid session."), errorString); + + qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to start the Waydroid session: " << errorString; + }); + + checkSessionStarting(10); +} + +void WaydroidDBusObject::stopSession() +{ + if (m_sessionStatus == SessionStopped) { + return; + } + + const QStringList arguments{u"session"_s, u"stop"_s}; QProcess *process = new QProcess(this); process->start(WAYDROID_COMMAND, arguments); process->waitForFinished(); - const int exitCode = process->exitCode(); + if (process->exitCode() == 0) { + m_sessionStatus = SessionStopped; + Q_EMIT sessionStatusChanged(); + } else { + qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to stop the Waydroid session: " << process->readAllStandardError(); + } +} + +void WaydroidDBusObject::resetWaydroid() +{ + if (m_status != Initialized || m_sessionStatus == SessionStarting) { + return; + } + + m_status = Resetting; + Q_EMIT statusChanged(); + + if (m_sessionStatus == SessionRunning) { + stopSession(); + } + + const QVariantMap args = {{u"homeDir"_s, QDir::homePath()}}; + + KAuth::Action writeAction(u"org.kde.plasma.mobileshell.waydroidhelper.reset"_s); + writeAction.setHelperId(u"org.kde.plasma.mobileshell.waydroidhelper"_s); + writeAction.setArguments(args); + + KAuth::ExecuteJob *job = writeAction.execute(); + job->start(); + + connect(job, &KAuth::ExecuteJob::finished, this, [this](KJob *job, auto) { + removeWaydroidApplications(); + + if (job->error() == 0) { + m_status = NotInitialized; + } else { + Q_EMIT errorOccurred(i18n("Failed to reset Waydroid."), ""); + + m_status = Initialized; + qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "KAuth returned an error code:" << job->error() << " message: " << job->errorString(); + } + + Q_EMIT statusChanged(); + }); +} + +void WaydroidDBusObject::installApk(const QString apkFile) +{ + const QStringList arguments{u"app"_s, u"install"_s, apkFile}; + + QProcess *process = new QProcess(this); + process->start(WAYDROID_COMMAND, arguments); + + connect(process, &QProcess::finished, this, [this, apkFile, process](int exitCode, QProcess::ExitStatus exitStatus) { + if (exitCode == 0 && exitStatus == QProcess::NormalExit) { + Q_EMIT actionFinished(i18n("Application has been installed")); + } else { + Q_EMIT actionFailed(i18n("Installation Failed")); + qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Error occurred during installation of " << apkFile << ": " << process->readAllStandardError(); + } + }); +} + +void WaydroidDBusObject::deleteApplication(const QString appId) +{ + const QStringList arguments{u"app"_s, u"remove"_s, appId}; + + QProcess *process = new QProcess(this); + process->start(WAYDROID_COMMAND, arguments); + + connect(process, &QProcess::finished, this, [this, appId, process](int exitCode, QProcess::ExitStatus exitStatus) { + Q_UNUSED(exitCode); + Q_UNUSED(exitStatus); + + const QByteArray errorLog = process->readAllStandardError(); + + // "waydroid app remove" send log on stderr but keep exitCode to 0 + if (errorLog.isEmpty()) { + Q_EMIT actionFinished(i18n("Application has been deleted")); + } else { + Q_EMIT actionFailed(i18n("Application uninstall failed")); + qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Error occurred during uninstallation of " << appId << ": " << errorLog; + } + }); +} + +int WaydroidDBusObject::status() const +{ + return m_status; +} + +int WaydroidDBusObject::sessionStatus() const +{ + return m_sessionStatus; +} + +int WaydroidDBusObject::systemType() const +{ + return m_systemType; +} + +QString WaydroidDBusObject::ipAddress() const +{ + return m_ipAddress; +} + +QString WaydroidDBusObject::androidId() const +{ + return m_androidId; +} + +bool WaydroidDBusObject::multiWindows() const +{ + return m_multiWindows; +} + +void WaydroidDBusObject::setMultiWindows(const bool multiWindows) +{ + if (m_multiWindows == multiWindows) { + return; + } + + const QString value = multiWindows ? "true" : "false"; + + if (writePropValue(MULTI_WINDOWS_PROP_KEY, value)) { + m_multiWindows = multiWindows; + Q_EMIT multiWindowsChanged(); + } +} + +bool WaydroidDBusObject::suspend() const +{ + return m_suspend; +} + +void WaydroidDBusObject::setSuspend(const bool suspend) +{ + if (m_suspend == suspend) { + return; + } + + const QString value = suspend ? "true" : "false"; + + if (writePropValue(SUSPEND_PROP_KEY, value)) { + m_suspend = suspend; + Q_EMIT suspendChanged(); + } +} + +bool WaydroidDBusObject::uevent() const +{ + return m_uevent; +} + +void WaydroidDBusObject::setUevent(const bool uevent) +{ + if (m_uevent == uevent) { + return; + } + + const QString value = uevent ? "true" : "false"; + + if (writePropValue(UEVENT_PROP_KEY, value)) { + m_uevent = uevent; + Q_EMIT ueventChanged(); + } +} + +QList WaydroidDBusObject::applications() const +{ + QList paths; + for (const auto &app : m_applicationObjects) { + paths.push_back(app->objectPath()); + } + return paths; +} + +void WaydroidDBusObject::refreshSupportsInfo() +{ + const QStringList arguments{u"-h"_s}; + + auto process = QProcess(this); + process.start(WAYDROID_COMMAND, arguments); + process.waitForFinished(); + + const int exitCode = process.exitCode(); if (exitCode != 0) { m_status = NotSupported; Q_EMIT statusChanged(); @@ -72,7 +373,7 @@ void WaydroidState::refreshSupportsInfo() Q_EMIT statusChanged(); } -void WaydroidState::refreshInstallationInfo() +void WaydroidDBusObject::refreshInstallationInfo() { if (m_status != Initialized) { return; @@ -99,7 +400,7 @@ void WaydroidState::refreshInstallationInfo() Q_EMIT systemTypeChanged(); } -void WaydroidState::refreshSessionInfo() +void WaydroidDBusObject::refreshSessionInfo() { if (m_status != Initialized) { return; @@ -108,7 +409,7 @@ void WaydroidState::refreshSessionInfo() const QString output = fetchSessionInfo(); const QString sessionMatchResult = extractRegExp(output, sessionRegExp); - WaydroidState::SessionStatus newSessionStatus; + SessionStatus newSessionStatus; if (!sessionMatchResult.isEmpty()) { newSessionStatus = sessionMatchResult.contains("RUNNING") ? SessionRunning : SessionStopped; @@ -125,7 +426,18 @@ void WaydroidState::refreshSessionInfo() Q_EMIT ipAddressChanged(); } -void WaydroidState::refreshAndroidId() +QString WaydroidDBusObject::fetchSessionInfo() +{ + const QStringList arguments{u"status"_s}; + + auto process = QProcess(this); + process.start(WAYDROID_COMMAND, arguments); + process.waitForFinished(); + + return process.readAllStandardOutput(); +} + +void WaydroidDBusObject::refreshAndroidId() { if (m_status != Initialized) { return; @@ -150,7 +462,7 @@ void WaydroidState::refreshAndroidId() }); } -void WaydroidState::refreshPropsInfo() +void WaydroidDBusObject::refreshPropsInfo() { if (m_sessionStatus != SessionRunning) { return; @@ -169,322 +481,7 @@ void WaydroidState::refreshPropsInfo() Q_EMIT ueventChanged(); } -void WaydroidState::resetError() -{ - m_errorTitle = ""; - Q_EMIT errorTitleChanged(); - - if (m_errorMessage != "") { - m_errorMessage = ""; - Q_EMIT errorMessageChanged(); - } -} - -QCoro::QmlTask WaydroidState::initializeQml(const SystemType systemType, const RomType romType, const bool forced) -{ - return initialize(systemType, romType, forced); -} - -QCoro::Task WaydroidState::initialize(const SystemType systemType, const RomType romType, const bool forced) -{ - if (m_status == Initializing) { - co_return; - } - - m_status = Initializing; - Q_EMIT statusChanged(); - - QString systemTypeArg; - switch (systemType) { - case SystemType::Vanilla: - systemTypeArg = "VANILLA"; - break; - case SystemType::Foss: - systemTypeArg = "FOSS"; - break; - case SystemType::Gapps: - systemTypeArg = "GAPPS"; - break; - default: - systemTypeArg = "VANILLA"; - break; - } - - QString romTypeArg; - switch (romType) { - case RomType::Lineage: - romTypeArg = "lineage"; - break; - case RomType::Bliss: - romTypeArg = "bliss"; - break; - } - - const QVariantMap args = {{u"systemType"_s, systemTypeArg}, {u"romType"_s, romTypeArg}, {u"forced"_s, forced}}; - - KAuth::Action writeAction(u"org.kde.plasma.mobileshell.waydroidhelper.initialize"_s); - writeAction.setHelperId(u"org.kde.plasma.mobileshell.waydroidhelper"_s); - writeAction.setArguments(args); - writeAction.setTimeout(3600000); // HACK: 1 hour to wait installation - - KAuth::ExecuteJob *job = writeAction.execute(); - job->start(); - - connect(job, &KAuth::ExecuteJob::newData, this, [this](const QVariantMap &data) { - QString log = data.value("log", "").toString(); - float downloaded = data.value("downloaded", 0.0).toFloat(); - float total = data.value("total", 0.0).toFloat(); - float speed = data.value("speed", 0.0).toFloat(); - - qCDebug(WAYDROIDINTEGRATIONPLUGIN) << "log: " << log; - Q_EMIT downloadStatusChanged(downloaded, total, speed); - }); - - co_await qCoro(job, &KAuth::ExecuteJob::finished); - - if (job->error() == 0) { - m_status = Initialized; - } else { - m_errorTitle = i18n("Failed to initialize Waydroid."); - Q_EMIT errorTitleChanged(); - m_errorMessage = job->errorString(); - Q_EMIT errorMessageChanged(); - - m_status = NotInitialized; - qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "KAuth returned an error code:" << job->error() << " message: " << job->errorString(); - } - - Q_EMIT statusChanged(); -} - -QCoro::QmlTask WaydroidState::startSessionQml() -{ - return startSession(); -} - -QCoro::Task WaydroidState::startSession() -{ - if (m_sessionStatus == SessionStarting || m_sessionStatus == SessionRunning) { - co_return; - } - - m_sessionStatus = SessionStarting; - Q_EMIT sessionStatusChanged(); - - const QStringList arguments{u"session"_s, u"start"_s}; - - QProcess *basicProcess = new QProcess(this); - auto process = qCoro(basicProcess); - co_await process.start(WAYDROID_COMMAND, arguments); - - connect(basicProcess, &QProcess::finished, this, [this, basicProcess](int exitCode, QProcess::ExitStatus exitStatus) { - Q_UNUSED(exitStatus); - - if (exitCode == 0) { - return; - } - - m_sessionStatus = SessionStopped; - Q_EMIT sessionStatusChanged(); - - QByteArray errorData = basicProcess->readAllStandardError(); - QString errorString = QString::fromUtf8(errorData); - - m_errorTitle = i18n("Failed to start the Waydroid session."); - Q_EMIT errorTitleChanged(); - m_errorMessage = errorString; - Q_EMIT errorMessageChanged(); - - qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to start the Waydroid session: " << errorString; - }); - - checkSessionStarting(10); -} - -QCoro::QmlTask WaydroidState::stopSessionQml() -{ - return stopSession(); -} - -QCoro::Task WaydroidState::stopSession() -{ - if (m_sessionStatus == SessionStopped) { - co_return; - } - - const QStringList arguments{u"session"_s, u"stop"_s}; - - QProcess basicProcess = QProcess(this); - auto process = qCoro(basicProcess); - co_await process.start(WAYDROID_COMMAND, arguments); - co_await process.waitForFinished(); - - if (basicProcess.exitCode() == 0) { - m_sessionStatus = SessionStopped; - Q_EMIT sessionStatusChanged(); - } else { - qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to stop the Waydroid session: " << basicProcess.readAllStandardError(); - } -} - -void WaydroidState::copyToClipboard(const QString text) -{ - qGuiApp->clipboard()->setText(text); -} - -QCoro::QmlTask WaydroidState::resetWaydroidQml() -{ - return resetWaydroid(); -} - -QCoro::Task WaydroidState::resetWaydroid() -{ - if (m_status != Initialized || m_sessionStatus == SessionStarting) { - co_return; - } - - m_status = Resetting; - Q_EMIT statusChanged(); - - if (m_sessionStatus == SessionRunning) { - co_await stopSession(); - } - - const QVariantMap args = {{u"homeDir"_s, QDir::homePath()}}; - - KAuth::Action writeAction(u"org.kde.plasma.mobileshell.waydroidhelper.reset"_s); - writeAction.setHelperId(u"org.kde.plasma.mobileshell.waydroidhelper"_s); - writeAction.setArguments(args); - - KAuth::ExecuteJob *job = writeAction.execute(); - job->start(); - - co_await qCoro(job, &KAuth::ExecuteJob::finished); - - removeWaydroidApplications(); - - if (job->error() == 0) { - m_status = NotInitialized; - } else { - m_errorTitle = i18n("Failed to reset Waydroid."); - Q_EMIT errorTitleChanged(); - - m_status = Initialized; - qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "KAuth returned an error code:" << job->error() << " message: " << job->errorString(); - } - - Q_EMIT statusChanged(); -} - -WaydroidState::Status WaydroidState::status() const -{ - return m_status; -} - -WaydroidState::SessionStatus WaydroidState::sessionStatus() const -{ - return m_sessionStatus; -} - -WaydroidState::SystemType WaydroidState::systemType() const -{ - return m_systemType; -} - -QString WaydroidState::ipAddress() const -{ - return m_ipAddress; -} - -QString WaydroidState::errorTitle() const -{ - return m_errorTitle; -} - -QString WaydroidState::errorMessage() const -{ - return m_errorMessage; -} - -QString WaydroidState::androidId() const -{ - return m_androidId; -} - -WaydroidApplicationListModel *WaydroidState::applicationListModel() const -{ - return m_applicationListModel; -} - -bool WaydroidState::multiWindows() const -{ - return m_multiWindows; -} - -void WaydroidState::setMultiWindows(const bool multiWindows) -{ - if (m_multiWindows == multiWindows) { - return; - } - - const QString value = multiWindows ? "true" : "false"; - - if (writePropValue(MULTI_WINDOWS_PROP_KEY, value)) { - m_multiWindows = multiWindows; - Q_EMIT multiWindowsChanged(); - } -} - -bool WaydroidState::suspend() const -{ - return m_suspend; -} - -void WaydroidState::setSuspend(const bool suspend) -{ - if (m_suspend == suspend) { - return; - } - - const QString value = suspend ? "true" : "false"; - - if (writePropValue(SUSPEND_PROP_KEY, value)) { - m_suspend = suspend; - Q_EMIT suspendChanged(); - } -} - -bool WaydroidState::uevent() const -{ - return m_uevent; -} - -void WaydroidState::setUevent(const bool uevent) -{ - if (m_uevent == uevent) { - return; - } - - const QString value = uevent ? "true" : "false"; - - if (writePropValue(UEVENT_PROP_KEY, value)) { - m_uevent = uevent; - Q_EMIT ueventChanged(); - } -} - -QString WaydroidState::fetchSessionInfo() -{ - const QStringList arguments{u"status"_s}; - - QProcess *process = new QProcess(this); - process->start(WAYDROID_COMMAND, arguments); - process->waitForFinished(); - - return process->readAllStandardOutput(); -} - -QString WaydroidState::fetchPropValue(const QString key, const QString defaultValue) +QString WaydroidDBusObject::fetchPropValue(const QString key, const QString defaultValue) { const QStringList arguments{u"prop"_s, u"get"_s, key}; @@ -502,18 +499,18 @@ QString WaydroidState::fetchPropValue(const QString key, const QString defaultVa return value; } -bool WaydroidState::writePropValue(const QString key, const QString value) +bool WaydroidDBusObject::writePropValue(const QString key, const QString value) { const QStringList arguments{u"prop"_s, u"set"_s, key, value}; - QProcess *process = new QProcess(this); - process->start(WAYDROID_COMMAND, arguments); - process->waitForFinished(); + auto process = QProcess(this); + process.start(WAYDROID_COMMAND, arguments); + process.waitForFinished(); - return process->exitCode() == 0; + return process.exitCode() == 0; } -QString WaydroidState::extractRegExp(const QString text, const QRegularExpression regExp) const +QString WaydroidDBusObject::extractRegExp(const QString text, const QRegularExpression regExp) const { const QRegularExpressionMatch match = regExp.match(text); @@ -524,7 +521,7 @@ QString WaydroidState::extractRegExp(const QString text, const QRegularExpressio } } -void WaydroidState::checkSessionStarting(const int limit, const int tried) +void WaydroidDBusObject::checkSessionStarting(const int limit, const int tried) { if (m_sessionStatus != SessionStarting) { return; @@ -547,7 +544,7 @@ void WaydroidState::checkSessionStarting(const int limit, const int tried) } } -QString WaydroidState::desktopFileDirectory() +QString WaydroidDBusObject::desktopFileDirectory() { auto dir = []() -> QString { if (KSandbox::isFlatpak()) { @@ -561,7 +558,7 @@ QString WaydroidState::desktopFileDirectory() return dir; } -bool WaydroidState::removeWaydroidApplications() +bool WaydroidDBusObject::removeWaydroidApplications() { const QDir appsDir(desktopFileDirectory()); const auto fileInfos = appsDir.entryInfoList(QDir::Files); @@ -595,3 +592,86 @@ bool WaydroidState::removeWaydroidApplications() return allFileRemoved; } + +void WaydroidDBusObject::refreshApplications() +{ + if (m_sessionStatus != SessionRunning) { + // Clear existing applications when session is not running + for (const auto &appObject : m_applicationObjects) { + appObject->unregisterObject(); + } + m_applicationObjects.clear(); + return; + } + + const QString output = fetchApplicationsList(); + if (output.isEmpty()) { + return; + } + + QTextStream inFile(const_cast(&output), QIODevice::ReadOnly); + const auto newApplications = WaydroidApplicationDBusObject::parseApplicationsFromWaydroidLog(inFile); + + // Create a map of existing applications by package name for efficient lookup + QMap existingAppMap; + for (int i = 0; i < m_applicationObjects.size(); ++i) { + const auto &application = m_applicationObjects[i]; + existingAppMap.insert(application->packageName(), i); + } + + QList toInsert; + + // Check which applications need to be added or are already present + for (const auto &application : newApplications) { + if (!application->name().isEmpty() && !application->packageName().isEmpty()) { + auto it = existingAppMap.find(application->packageName()); + if (it != existingAppMap.end()) { + // Application already exists, remove from map to mark as kept + existingAppMap.erase(it); + } else { + // Application needs to be inserted + toInsert.append(application); + } + } + } + + // Remove applications that are no longer present + QList toRemove; + for (const int index : existingAppMap.values()) { + toRemove.append(index); + } + + std::sort(toRemove.begin(), toRemove.end()); + + // Remove indices from end to start to avoid index shifting + for (int i = toRemove.size() - 1; i >= 0; --i) { + int ind = toRemove[i]; + const auto application = m_applicationObjects[ind]; + m_applicationObjects.removeAt(ind); + Q_EMIT applicationRemoved(application->objectPath()); + application->unregisterObject(); + } + + // Add new applications and register them + for (const auto &application : toInsert) { + application->registerObject(); + m_applicationObjects.append(application); + Q_EMIT applicationAdded(application->objectPath()); + } +} + +QString WaydroidDBusObject::fetchApplicationsList() +{ + const QStringList arguments{u"app"_s, u"list"_s}; + + auto process = QProcess(this); + process.start(WAYDROID_COMMAND, arguments); + process.waitForFinished(); + + if (process.exitCode() != 0) { + qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to fetch applications list: " << process.readAllStandardError(); + return QString{}; + } + + return process.readAllStandardOutput(); +} diff --git a/components/waydroidintegrationplugin/waydroidstate.h b/components/waydroidintegrationplugin/waydroiddbusobject.h similarity index 60% rename from components/waydroidintegrationplugin/waydroidstate.h rename to components/waydroidintegrationplugin/waydroiddbusobject.h index bb37e2c5..1ebdc0c2 100644 --- a/components/waydroidintegrationplugin/waydroidstate.h +++ b/components/waydroidintegrationplugin/waydroiddbusobject.h @@ -6,16 +6,16 @@ #pragma once -#include "waydroidapplicationlistmodel.h" +#include "waydroidapplicationdbusobject.h" -#include -#include +#include +#include #include +#include #include -#include -class WaydroidApplicationListModel; +class WaydroidApplicationDBusObject; /** * This class provides an interface to interact with the Waydroid container, @@ -23,26 +23,15 @@ class WaydroidApplicationListModel; * * @author Florian RICHER */ -class WaydroidState : public QObject +class WaydroidDBusObject : public QObject { Q_OBJECT QML_ELEMENT QML_SINGLETON - - Q_PROPERTY(Status status READ status NOTIFY statusChanged) - Q_PROPERTY(SessionStatus sessionStatus READ sessionStatus NOTIFY sessionStatusChanged) - Q_PROPERTY(SystemType systemType READ systemType NOTIFY systemTypeChanged) - Q_PROPERTY(QString ipAddress READ ipAddress NOTIFY ipAddressChanged) - Q_PROPERTY(QString androidId READ androidId NOTIFY androidIdChanged) - Q_PROPERTY(QString errorTitle READ errorTitle NOTIFY errorTitleChanged) - Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged) - Q_PROPERTY(bool multiWindows READ multiWindows WRITE setMultiWindows NOTIFY multiWindowsChanged) - Q_PROPERTY(bool suspend READ suspend WRITE setSuspend NOTIFY suspendChanged) - Q_PROPERTY(bool uevent READ uevent WRITE setUevent NOTIFY ueventChanged) - Q_PROPERTY(WaydroidApplicationListModel *applicationListModel READ applicationListModel CONSTANT) + Q_CLASSINFO("D-Bus Interface", "org.kde.plasmashell.Waydroid") public: - WaydroidState(QObject *parent = nullptr); + explicit WaydroidDBusObject(QObject *parent = nullptr); /** * @enum Status @@ -92,68 +81,72 @@ public: }; Q_ENUM(RomType) - Q_INVOKABLE void refreshSupportsInfo(); - Q_INVOKABLE void refreshInstallationInfo(); - Q_INVOKABLE void refreshSessionInfo(); - Q_INVOKABLE void refreshAndroidId(); - Q_INVOKABLE void refreshPropsInfo(); - Q_INVOKABLE void resetError(); - Q_INVOKABLE QCoro::QmlTask initializeQml(const SystemType systemType, const RomType romType, const bool forced = false); - QCoro::Task initialize(const SystemType systemType, const RomType romType, const bool forced = false); - Q_INVOKABLE QCoro::QmlTask startSessionQml(); - QCoro::Task startSession(); - Q_INVOKABLE QCoro::QmlTask stopSessionQml(); - QCoro::Task stopSession(); - Q_INVOKABLE QCoro::QmlTask resetWaydroidQml(); - QCoro::Task resetWaydroid(); - - Q_INVOKABLE void copyToClipboard(const QString text); - - Status status() const; - SessionStatus sessionStatus() const; - SystemType systemType() const; - QString ipAddress() const; - QString androidId() const; - QString errorTitle() const; - QString errorMessage() const; - WaydroidApplicationListModel *applicationListModel() const; - - bool multiWindows() const; - void setMultiWindows(const bool multiWindows); - bool suspend() const; - void setSuspend(const bool suspend); - bool uevent() const; - void setUevent(const bool uevent); + // called by QML + Q_INVOKABLE void registerObject(); Q_SIGNALS: - void statusChanged(); + Q_SCRIPTABLE void statusChanged(); // download and total is in MB and speed in Kbps - void downloadStatusChanged(float downloaded, float total, float speed); - void sessionStatusChanged(); - void systemTypeChanged(); - void ipAddressChanged(); - void multiWindowsChanged(); - void suspendChanged(); - void ueventChanged(); - void errorTitleChanged(); - void errorMessageChanged(); - void androidIdChanged(); + Q_SCRIPTABLE void downloadStatusChanged(double downloaded, double total, double speed); + Q_SCRIPTABLE void sessionStatusChanged(); + Q_SCRIPTABLE void systemTypeChanged(); + Q_SCRIPTABLE void ipAddressChanged(); + Q_SCRIPTABLE void androidIdChanged(); + Q_SCRIPTABLE void multiWindowsChanged(); + Q_SCRIPTABLE void suspendChanged(); + Q_SCRIPTABLE void ueventChanged(); + + Q_SCRIPTABLE void applicationAdded(QDBusObjectPath path); + Q_SCRIPTABLE void applicationRemoved(QDBusObjectPath path); + + // Use to display banner + Q_SCRIPTABLE void actionFinished(const QString message); + Q_SCRIPTABLE void actionFailed(const QString message); + + // General error + Q_SCRIPTABLE void errorOccurred(const QString title, const QString message); + +public Q_SLOTS: + Q_SCRIPTABLE int status() const; + Q_SCRIPTABLE int sessionStatus() const; + Q_SCRIPTABLE int systemType() const; + Q_SCRIPTABLE QString ipAddress() const; + Q_SCRIPTABLE QString androidId() const; + Q_SCRIPTABLE bool multiWindows() const; + Q_SCRIPTABLE void setMultiWindows(const bool multiWindows); + Q_SCRIPTABLE bool suspend() const; + Q_SCRIPTABLE void setSuspend(const bool suspend); + Q_SCRIPTABLE bool uevent() const; + Q_SCRIPTABLE void setUevent(const bool uevent); + Q_SCRIPTABLE QList applications() const; + + Q_SCRIPTABLE void initialize(const int systemType, const int romType, const bool forced = false); + Q_SCRIPTABLE void startSession(); + Q_SCRIPTABLE void stopSession(); + Q_SCRIPTABLE void resetWaydroid(); + Q_SCRIPTABLE void installApk(const QString apkFile); + Q_SCRIPTABLE void deleteApplication(const QString appId); + Q_SCRIPTABLE void refreshSessionInfo(); + Q_SCRIPTABLE void refreshAndroidId(); + Q_SCRIPTABLE void refreshApplications(); private: + bool m_dbusInitialized{false}; Status m_status{NotInitialized}; SessionStatus m_sessionStatus{SessionStopped}; - SystemType m_systemType{SystemType::UnknownSystemType}; + SystemType m_systemType{UnknownSystemType}; QString m_ipAddress{""}; - QString m_errorTitle{""}; - QString m_errorMessage{""}; QString m_androidId{""}; - WaydroidApplicationListModel *m_applicationListModel{nullptr}; // Waydroid props. See https://docs.waydro.id/usage/waydroid-prop-options bool m_multiWindows{false}; bool m_suspend{false}; bool m_uevent{false}; + void refreshSupportsInfo(); + void refreshInstallationInfo(); + void refreshPropsInfo(); + /** * @brief Executes the command to retrieve the current session status and related * information from Waydroid. @@ -208,4 +201,7 @@ private: QString desktopFileDirectory(); bool removeWaydroidApplications(); -}; + + QString fetchApplicationsList(); + QList m_applicationObjects; +}; \ No newline at end of file diff --git a/kcms/waydroidintegration/ui/WaydroidApplicationsPage.qml b/kcms/waydroidintegration/ui/WaydroidApplicationsPage.qml index c8c9252b..2e193cf6 100644 --- a/kcms/waydroidintegration/ui/WaydroidApplicationsPage.qml +++ b/kcms/waydroidintegration/ui/WaydroidApplicationsPage.qml @@ -34,7 +34,7 @@ KCM.SimpleKCM { ] Connections { - target: AIP.WaydroidState.applicationListModel + target: AIP.WaydroidDBusClient function onActionFinished(message: string): void { inlineMessage.text = message @@ -42,13 +42,21 @@ KCM.SimpleKCM { inlineMessage.type = Kirigami.MessageType.Positive } - function onErrorOccurred(error: string): void { + function onActionFailed(error: string): void { inlineMessage.text = error inlineMessage.visible = true inlineMessage.type = Kirigami.MessageType.Error } } + Timer { + id: autoRefreshApplicationsTimer + interval: 2000 + repeat: true + running: root.visible + onTriggered: AIP.WaydroidDBusClient.refreshApplications() + } + FileDialog { id: fileDialog nameFilters: [ "APK files (*.apk)" ] @@ -60,7 +68,7 @@ KCM.SimpleKCM { inlineMessage.visible = true inlineMessage.type = Kirigami.MessageType.Error } else { - AIP.WaydroidState.applicationListModel.installApk(url.pathname) + AIP.WaydroidDBusClient.installApk(url.pathname) } } } @@ -79,7 +87,7 @@ KCM.SimpleKCM { FormCard.FormCard { Repeater { - model: AIP.WaydroidState.applicationListModel + model: AIP.WaydroidDBusClient.applicationListModel delegate: FormCard.AbstractFormDelegate { id: appDelegate @@ -99,7 +107,7 @@ KCM.SimpleKCM { text: i18nc("@action:button", "Delete the application") icon.name: "usermenu-delete" - onClicked: AIP.WaydroidState.applicationListModel.deleteApplication(model.id) + onClicked: AIP.WaydroidDBusClient.deleteApplication(model.id) QQC2.ToolTip.visible: hovered QQC2.ToolTip.text: text diff --git a/kcms/waydroidintegration/ui/WaydroidConfigurationForm.qml b/kcms/waydroidintegration/ui/WaydroidConfigurationForm.qml index 9c1f180f..1ee15538 100644 --- a/kcms/waydroidintegration/ui/WaydroidConfigurationForm.qml +++ b/kcms/waydroidintegration/ui/WaydroidConfigurationForm.qml @@ -22,12 +22,12 @@ ColumnLayout { FormCard.FormCard { FormCard.FormTextDelegate { text: i18n("IP address") - description: AIP.WaydroidState.ipAddress + description: AIP.WaydroidDBusClient.ipAddress trailing: PC3.Button { - visible: AIP.WaydroidState.ipAddress !== "" + visible: AIP.WaydroidDBusClient.ipAddress !== "" text: i18n("Copy") icon.name: 'edit-copy-symbolic' - onClicked: AIP.WaydroidState.copyToClipboard(AIP.WaydroidState.ipAddress) + onClicked: AIP.WaydroidDBusClient.copyToClipboard(AIP.WaydroidDBusClient.ipAddress) } } @@ -37,12 +37,12 @@ ColumnLayout { trailing: PC3.Button { text: i18n("Stop session") - onClicked: AIP.WaydroidState.stopSessionQml() + onClicked: AIP.WaydroidDBusClient.stopSession() } } FormCard.FormButtonDelegate { - visible: AIP.WaydroidState.systemType === AIP.WaydroidState.Gapps + visible: AIP.WaydroidDBusClient.systemType === AIP.WaydroidDBusClient.Gapps text: i18n("Certify my device for Google Play Protect") onClicked: kcm.push("WaydroidGooglePlayProtectConfigurationPage.qml") } @@ -63,7 +63,7 @@ ColumnLayout { subtitle: i18n("Are you sure you want to reset Waydroid ? This is a destructive action, and will wipe all user data.") standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel - onAccepted: AIP.WaydroidState.resetWaydroidQml() + onAccepted: AIP.WaydroidDBusClient.resetWaydroid() } } @@ -74,7 +74,7 @@ ColumnLayout { interval: 2000 repeat: true running: root.visible - onTriggered: AIP.WaydroidState.refreshSessionInfo() + onTriggered: AIP.WaydroidDBusClient.refreshSessionInfo() } FormCard.FormHeader { @@ -96,7 +96,7 @@ ColumnLayout { } Connections { - target: AIP.WaydroidState + target: AIP.WaydroidDBusClient function onSessionStatusChanged() { infoMessage.visible = false @@ -108,9 +108,9 @@ ColumnLayout { id: multiWindows text: i18n("Multi Windows") description: i18n("Enables/Disables window integration with the desktop") - checked: AIP.WaydroidState.multiWindows + checked: AIP.WaydroidDBusClient.multiWindows onToggled: { - AIP.WaydroidState.multiWindows = checked + AIP.WaydroidDBusClient.multiWindows = checked infoMessage.visible = true } } @@ -121,9 +121,9 @@ ColumnLayout { id: suspend text: i18n("Suspend") description: i18n("Let the Waydroid container sleep (after the display timeout) when no apps are active") - checked: AIP.WaydroidState.suspend + checked: AIP.WaydroidDBusClient.suspend onToggled: { - AIP.WaydroidState.suspend = checked + AIP.WaydroidDBusClient.suspend = checked infoMessage.visible = true } } @@ -134,9 +134,9 @@ ColumnLayout { id: uevent text: i18n("UEvent") description: i18n("Allow android direct access to hotplugged devices") - checked: AIP.WaydroidState.uevent + checked: AIP.WaydroidDBusClient.uevent onToggled: { - AIP.WaydroidState.uevent = checked + AIP.WaydroidDBusClient.uevent = checked infoMessage.visible = true } } diff --git a/kcms/waydroidintegration/ui/WaydroidErrorPage.qml b/kcms/waydroidintegration/ui/WaydroidErrorPage.qml new file mode 100644 index 00000000..f0b4eb59 --- /dev/null +++ b/kcms/waydroidintegration/ui/WaydroidErrorPage.qml @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2025 Florian RICHER + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 as QQC2 + +import org.kde.kirigami 2.19 as Kirigami +import org.kde.plasma.components 3.0 as PC3 + +ColumnLayout { + id: root + + property string title + property string message + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent + anchors.leftMargin: Kirigami.Units.largeSpacing + anchors.right: parent + anchors.rightMargin: Kirigami.Units.largeSpacing + spacing: Kirigami.Units.largeSpacing + + QQC2.Label { + text: title + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + } + + QQC2.TextArea { + visible: message !== "" + text: message + readOnly: true + wrapMode: TextEdit.Wrap + Layout.fillWidth: true + } +} diff --git a/kcms/waydroidintegration/ui/WaydroidGooglePlayProtectConfigurationPage.qml b/kcms/waydroidintegration/ui/WaydroidGooglePlayProtectConfigurationPage.qml index 0cc2b0dd..8372ae8a 100644 --- a/kcms/waydroidintegration/ui/WaydroidGooglePlayProtectConfigurationPage.qml +++ b/kcms/waydroidintegration/ui/WaydroidGooglePlayProtectConfigurationPage.qml @@ -23,18 +23,18 @@ KCM.SimpleKCM { title: i18n("Google Play Protect configuration") Component.onCompleted: { - if (AIP.WaydroidState.androidId === "") { - AIP.WaydroidState.refreshAndroidId() + if (AIP.WaydroidDBusClient.androidId === "") { + AIP.WaydroidDBusClient.refreshAndroidId() } } WaydroidLoader { - visible: AIP.WaydroidState.androidId === "" + visible: AIP.WaydroidDBusClient.androidId === "" text: i18n("We fetching your Android ID.\nIt can take a few seconds.") } ColumnLayout { - visible: AIP.WaydroidState.androidId !== "" + visible: AIP.WaydroidDBusClient.androidId !== "" anchors.verticalCenter: parent.verticalCenter anchors.left: parent anchors.leftMargin: Kirigami.Units.largeSpacing @@ -52,7 +52,7 @@ KCM.SimpleKCM { icon.name: 'edit-copy-symbolic' Layout.alignment: Qt.AlignHCenter onClicked: { - AIP.WaydroidState.copyToClipboard(AIP.WaydroidState.androidId) + AIP.WaydroidDBusClient.copyToClipboard(AIP.WaydroidDBusClient.androidId) Qt.openUrlExternally("https://www.google.com/android/uncertified") } } diff --git a/kcms/waydroidintegration/ui/WaydroidInitialConfigurationForm.qml b/kcms/waydroidintegration/ui/WaydroidInitialConfigurationForm.qml index 3a2a39a7..c7c2bc4c 100644 --- a/kcms/waydroidintegration/ui/WaydroidInitialConfigurationForm.qml +++ b/kcms/waydroidintegration/ui/WaydroidInitialConfigurationForm.qml @@ -22,8 +22,8 @@ ColumnLayout { text: i18n("System type") model: [ - {"name": "Vanilla", "value": AIP.WaydroidState.Vanilla}, - {"name": "GAPPS", "value": AIP.WaydroidState.Gapps} + {"name": "Vanilla", "value": AIP.WaydroidDBusClient.Vanilla}, + {"name": "GAPPS", "value": AIP.WaydroidDBusClient.Gapps} ] textRole: "name" @@ -36,6 +36,6 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter enabled: systemType.currentValue !== undefined - onClicked: AIP.WaydroidState.initializeQml(systemType.currentValue, AIP.WaydroidState.Lineage) + onClicked: AIP.WaydroidDBusClient.initialize(systemType.currentValue, AIP.WaydroidDBusClient.Lineage) } } \ No newline at end of file diff --git a/kcms/waydroidintegration/ui/main.qml b/kcms/waydroidintegration/ui/main.qml index 943c9dc5..4ac62514 100644 --- a/kcms/waydroidintegration/ui/main.qml +++ b/kcms/waydroidintegration/ui/main.qml @@ -24,7 +24,7 @@ KCM.SimpleKCM { rightPadding: 0 ColumnLayout { - visible: AIP.WaydroidState.errorTitle === "" && AIP.WaydroidState.status == AIP.WaydroidState.NotSupported + visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.NotSupported anchors.centerIn: parent spacing: Kirigami.Units.largeSpacing @@ -38,21 +38,21 @@ KCM.SimpleKCM { PC3.Button { text: i18n("Check installation") Layout.alignment: Qt.AlignHCenter - onClicked: AIP.WaydroidState.refreshSupportsInfo() + onClicked: AIP.WaydroidDBusClient.refreshSupportsInfo() } } WaydroidInitialConfigurationForm { - visible: AIP.WaydroidState.errorTitle === "" && AIP.WaydroidState.status == AIP.WaydroidState.NotInitialized + visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.NotInitialized } WaydroidDownloadStatus { id: downloadStatus - visible: AIP.WaydroidState.errorTitle === "" && AIP.WaydroidState.status == AIP.WaydroidState.Initializing + visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initializing text: i18n("Downloading Android and vendor images.\nIt can take a few minutes.") Connections { - target: AIP.WaydroidState + target: AIP.WaydroidDBusClient function onDownloadStatusChanged(downloaded, total, speed) { downloadStatus.downloaded = downloaded @@ -63,12 +63,12 @@ KCM.SimpleKCM { } WaydroidLoader { - visible: AIP.WaydroidState.errorTitle === "" && AIP.WaydroidState.status == AIP.WaydroidState.Resetting + visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Resetting text: i18n("Waydroid is resetting.\nIt can take a few seconds.") } ColumnLayout { - visible: AIP.WaydroidState.errorTitle === "" && AIP.WaydroidState.status == AIP.WaydroidState.Initialized && AIP.WaydroidState.sessionStatus == AIP.WaydroidState.SessionStopped + visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized && AIP.WaydroidDBusClient.sessionStatus === AIP.WaydroidDBusClient.SessionStopped anchors.centerIn: parent spacing: Kirigami.Units.largeSpacing @@ -81,46 +81,24 @@ KCM.SimpleKCM { PC3.Button { text: i18n("Start the session") Layout.alignment: Qt.AlignHCenter - onClicked: AIP.WaydroidState.startSessionQml() + onClicked: AIP.WaydroidDBusClient.startSession() } } WaydroidLoader { - visible: AIP.WaydroidState.errorTitle === "" && AIP.WaydroidState.status == AIP.WaydroidState.Initialized && AIP.WaydroidState.sessionStatus == AIP.WaydroidState.SessionStarting + visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized && AIP.WaydroidDBusClient.sessionStatus === AIP.WaydroidDBusClient.SessionStarting text: i18n("Waydroid session is starting.\nIt can take a few seconds.") } WaydroidConfigurationForm { - visible: AIP.WaydroidState.errorTitle === "" && AIP.WaydroidState.status == AIP.WaydroidState.Initialized && AIP.WaydroidState.sessionStatus == AIP.WaydroidState.SessionRunning + visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized && AIP.WaydroidDBusClient.sessionStatus === AIP.WaydroidDBusClient.SessionRunning } - ColumnLayout { - visible: AIP.WaydroidState.errorTitle !== "" - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent - anchors.leftMargin: Kirigami.Units.largeSpacing - anchors.right: parent - anchors.rightMargin: Kirigami.Units.largeSpacing - spacing: Kirigami.Units.largeSpacing + Connections { + target: AIP.WaydroidDBusClient - QQC2.Label { - text: AIP.WaydroidState.errorTitle - Layout.alignment: Qt.AlignHCenter - horizontalAlignment: Text.AlignHCenter - } - - QQC2.TextArea { - visible: AIP.WaydroidState.errorMessage !== "" - text: AIP.WaydroidState.errorMessage - readOnly: true - wrapMode: TextEdit.Wrap - Layout.fillWidth: true - } - - PC3.Button { - text: i18n("Go back") - Layout.alignment: Qt.AlignHCenter - onClicked: AIP.WaydroidState.resetError() + function onErrorOccurred(title, message) { + kcm.push("WaydroidErrorPage.qml", { title, message }) } } -} \ No newline at end of file +} diff --git a/quicksettings/waydroid/contents/ui/main.qml b/quicksettings/waydroid/contents/ui/main.qml index 696bf142..46cd8789 100644 --- a/quicksettings/waydroid/contents/ui/main.qml +++ b/quicksettings/waydroid/contents/ui/main.qml @@ -9,18 +9,36 @@ import org.kde.plasma.private.mobileshell.waydroidintegrationplugin as AIP QS.QuickSetting { text: i18nc("@action:button", "Waydroid") - status: AIP.WaydroidState.sessionStatus === AIP.WaydroidState.SessionRunning ? i18nc("@info:status", "Running") : i18nc("@info:status", "Stopped") + status: statusText() icon: "folder-android-symbolic" settingsCommand: "plasma-open-settings kcm_waydroidintegration" - enabled: AIP.WaydroidState.sessionStatus === AIP.WaydroidState.SessionRunning - available: AIP.WaydroidState.status === AIP.WaydroidState.Initialized + available: AIP.WaydroidDBusClient.status !== AIP.WaydroidDBusClient.NotSupported + enabled: AIP.WaydroidDBusClient.sessionStatus === AIP.WaydroidDBusClient.SessionRunning + + Component.onCompleted: { + AIP.WaydroidDBusObject.registerObject() + } function toggle(): void { - if (AIP.WaydroidState.sessionStatus === AIP.WaydroidState.SessionRunning) { - AIP.WaydroidState.stopSessionQml() + if (AIP.WaydroidDBusClient.status !== AIP.WaydroidDBusClient.Initialized) { + return + } + + if (AIP.WaydroidDBusClient.sessionStatus === AIP.WaydroidDBusClient.SessionRunning) { + AIP.WaydroidDBusClient.stopSession() } else { - AIP.WaydroidState.startSessionQml() + AIP.WaydroidDBusClient.startSession() + } + } + + function statusText(): string { + if (AIP.WaydroidDBusClient.status !== AIP.WaydroidDBusClient.Initialized) { + return i18nc("@info:status", "Not initialized") + } else if (AIP.WaydroidDBusClient.sessionStatus === AIP.WaydroidDBusClient.SessionRunning) { + return i18nc("@info:status", "Running") + } else { + return i18nc("@info:status", "Stopped") } } }