waydroid: Migrate to DBus backend implementations

This commit is contained in:
Florian RICHER 2025-08-06 20:06:17 +00:00
parent 6f862e24c3
commit 7ebb4aa37c
20 changed files with 1622 additions and 744 deletions

View file

@ -1,10 +1,51 @@
# SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
# 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
)

View file

@ -1,69 +0,0 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <waydroidapplication.h>
#include <QRegularExpression>
#include <QStringLiteral>
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<WaydroidApplication>();
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;
}

View file

@ -1,39 +0,0 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QDir>
#include <QObject>
#include <qtmetamacros.h>
class WaydroidApplication : public QObject, public std::enable_shared_from_this<WaydroidApplication>
{
Q_OBJECT
Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QString packageName READ packageName CONSTANT)
public:
typedef std::shared_ptr<WaydroidApplication> 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;
};

View file

@ -0,0 +1,103 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "waydroidapplicationdbusclient.h"
#include <QDBusConnection>
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<QString> 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<QString> reply = *watcher;
const auto packageName = reply.argumentAt<0>();
if (m_packageName != packageName) {
m_packageName = packageName;
Q_EMIT packageNameChanged();
}
});
}

View file

@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "plasmashellwaydroidapplicationinterface.h"
#include <QObject>
#include <QString>
#include <qqmlregistration.h>
/**
* This class provides a DBus client interface for individual Waydroid application.
* It connects to WaydroidApplicationDBusObject instances via DBus.
*
* @author Florian RICHER <florian.richer@protonmail.com>
*/
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<WaydroidApplicationDBusClient> 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();
};

View file

@ -0,0 +1,110 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "waydroidapplicationdbusobject.h"
#include "waydroidapplicationadaptor.h"
#include "waydroidintegrationplugin_debug.h"
#include <QDBusConnection>
#include <QLoggingCategory>
#include <QRegularExpression>
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<WaydroidApplicationDBusObject>();
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::Ptr> WaydroidApplicationDBusObject::parseApplicationsFromWaydroidLog(QTextStream &inFile)
{
QList<Ptr> 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;
}

View file

@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QDBusObjectPath>
#include <QList>
#include <QObject>
#include <QString>
#include <qqmlregistration.h>
/**
* 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 <florian.richer@protonmail.com>
*/
class WaydroidApplicationDBusObject : public QObject
{
Q_OBJECT
QML_ELEMENT
Q_CLASSINFO("D-Bus Interface", "org.kde.plasmashell.WaydroidApplication")
public:
typedef std::shared_ptr<WaydroidApplicationDBusObject> 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<Ptr> 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;
};

View file

@ -5,123 +5,79 @@
*/
#include "waydroidapplicationlistmodel.h"
#include "waydroidintegrationplugin_debug.h"
#include "waydroidshared.h"
#include <QLoggingCategory>
#include <QProcess>
#include <QStringLiteral>
#include "waydroidapplicationdbusclient.h"
#include "waydroiddbusclient.h"
#include <KLocalizedString>
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<WaydroidApplication::Ptr> applications)
void WaydroidApplicationListModel::initializeApplications(const QList<QDBusObjectPath> &applicationObjectPaths)
{
if (m_waydroidState->sessionStatus() != WaydroidState::SessionRunning) {
if (!m_applications.isEmpty()) {
return;
}
qCDebug(WAYDROIDINTEGRATIONPLUGIN) << "Reload waydroid apps";
QMap<QString, int> appIdMap; // <packageName, index>
for (int i = 0; i < m_applications.size(); ++i) {
const auto &application = m_applications[i];
appIdMap.insert(application->packageName(), i);
beginResetModel();
for (const QDBusObjectPath &applicationObjectPath : applicationObjectPaths) {
auto client = std::make_shared<WaydroidApplicationDBusClient>(applicationObjectPath, this);
m_applications.append(client);
}
endResetModel();
}
QList<WaydroidApplication::Ptr> 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<int> 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) {
void WaydroidApplicationListModel::addApplication(const QDBusObjectPath &objectPath)
{
beginInsertRows({}, m_applications.size(), m_applications.size());
m_applications.append(application);
auto client = std::make_shared<WaydroidApplicationDBusClient>(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::refreshApplications()
void WaydroidApplicationListModel::updateApplication(const QDBusObjectPath &objectPath, const QList<int> &roles)
{
QList<WaydroidApplication::Ptr> 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<WaydroidApplication::Ptr> 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);
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<int, QByteArray> 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;
}
});
}

View file

@ -6,14 +6,13 @@
#pragma once
#include "waydroidapplication.h"
#include "waydroidstate.h"
#include "waydroidapplicationdbusclient.h"
#include <QAbstractListModel>
#include <QObject>
#include <QTimer>
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<int, QByteArray> roleNames() const override;
Q_INVOKABLE void installApk(const QString apkFile);
Q_INVOKABLE void deleteApplication(const QString appId);
void initializeApplications(const QList<QDBusObjectPath> &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<WaydroidApplication::Ptr> m_applications;
WaydroidDBusClient *m_waydroidDBusClient{nullptr};
QList<WaydroidApplicationDBusClient::Ptr> m_applications;
QTimer *m_refreshTimer{nullptr};
void loadApplications(const QList<WaydroidApplication::Ptr> applications);
void refreshApplications();
void updateApplication(const QDBusObjectPath &objectPath, const QList<int> &roles);
};

View file

@ -0,0 +1,404 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "waydroiddbusclient.h"
#include <QClipboard>
#include <QCoroDBusPendingReply>
#include <QGuiApplication>
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<QList<QDBusObjectPath>> 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<void> 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<void> 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<void> 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<void> WaydroidDBusClient::refreshSessionInfoTask()
{
auto pendingReply = m_interface->refreshSessionInfo();
co_await pendingReply;
}
QCoro::QmlTask WaydroidDBusClient::refreshSessionInfo()
{
return refreshSessionInfoTask();
}
QCoro::Task<void> WaydroidDBusClient::refreshAndroidIdTask()
{
auto pendingReply = m_interface->refreshAndroidId();
co_await pendingReply;
}
QCoro::QmlTask WaydroidDBusClient::refreshAndroidId()
{
return refreshAndroidIdTask();
}
QCoro::Task<void> 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<void> 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<void> WaydroidDBusClient::startSessionTask()
{
auto pendingReply = m_interface->startSession();
co_await pendingReply;
}
QCoro::QmlTask WaydroidDBusClient::startSession()
{
return startSessionTask();
}
QCoro::Task<void> WaydroidDBusClient::stopSessionTask()
{
auto pendingReply = m_interface->stopSession();
co_await pendingReply;
}
QCoro::QmlTask WaydroidDBusClient::stopSession()
{
return stopSessionTask();
}
QCoro::Task<void> WaydroidDBusClient::resetWaydroidTask()
{
auto pendingReply = m_interface->resetWaydroid();
co_await pendingReply;
}
QCoro::QmlTask WaydroidDBusClient::resetWaydroid()
{
return resetWaydroidTask();
}
QCoro::Task<void> 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<void> 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<int> reply = *watcher;
const auto status = static_cast<Status>(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<int> reply = *watcher;
const auto sessionStatus = static_cast<SessionStatus>(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<int> reply = *watcher;
const auto systemType = static_cast<SystemType>(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<QString> 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<QString> 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<bool> 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<bool> 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<bool> 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);
}

View file

@ -0,0 +1,173 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "plasmashellwaydroidinterface.h"
#include "waydroidapplicationlistmodel.h"
#include "waydroiddbusobject.h"
#include <QCoroCore>
#include <QCoroQmlTask>
#include <QDBusServiceWatcher>
#include <QObject>
#include <QString>
#include <qqmlregistration.h>
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<void> initializeTask(const SystemType systemType, const RomType romType, const bool forced = false);
QCoro::Task<void> startSessionTask();
QCoro::Task<void> stopSessionTask();
QCoro::Task<void> resetWaydroidTask();
QCoro::Task<void> installApkTask(const QString apkFile);
QCoro::Task<void> deleteApplicationTask(const QString appId);
QCoro::Task<void> setMultiWindowsTask(const bool multiWindows);
QCoro::Task<void> setSuspendTask(const bool suspend);
QCoro::Task<void> setUeventTask(const bool uevent);
QCoro::Task<void> refreshSessionInfoTask();
QCoro::Task<void> refreshAndroidIdTask();
QCoro::Task<void> refreshApplicationsTask();
};

View file

@ -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 <QClipboard>
#include <QCoroProcess>
#include <QDebug>
#include <QDBusConnection>
#include <QDir>
#include <QGuiApplication>
#include <QLoggingCategory>
#include <QProcess>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTimer>
#include <QtLogging>
#include <KAuth/Action>
#include <KAuth/ExecuteJob>
#include <KConfigGroup>
#include <KDesktopFile>
@ -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}}
{
}
void WaydroidDBusObject::registerObject()
{
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, &WaydroidState::statusChanged, this, &WaydroidState::refreshSessionInfo);
connect(this, &WaydroidState::statusChanged, this, &WaydroidState::refreshInstallationInfo);
connect(this, &WaydroidState::sessionStatusChanged, this, &WaydroidState::refreshPropsInfo);
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 WaydroidState::refreshSupportsInfo()
void WaydroidDBusObject::initialize(const int systemType, const int romType, const bool forced)
{
const QStringList arguments{u"-h"_s};
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<QDBusObjectPath> WaydroidDBusObject::applications() const
{
QList<QDBusObjectPath> 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<void> 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<void> 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<void> 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<void> 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<QString *>(&output), QIODevice::ReadOnly);
const auto newApplications = WaydroidApplicationDBusObject::parseApplicationsFromWaydroidLog(inFile);
// Create a map of existing applications by package name for efficient lookup
QMap<QString, int> existingAppMap;
for (int i = 0; i < m_applicationObjects.size(); ++i) {
const auto &application = m_applicationObjects[i];
existingAppMap.insert(application->packageName(), i);
}
QList<WaydroidApplicationDBusObject::Ptr> 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<int> 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();
}

View file

@ -6,16 +6,16 @@
#pragma once
#include "waydroidapplicationlistmodel.h"
#include "waydroidapplicationdbusobject.h"
#include <QCoroCore>
#include <QCoroQmlTask>
#include <QDBusObjectPath>
#include <QList>
#include <QObject>
#include <QString>
#include <qqmlregistration.h>
#include <qtmetamacros.h>
class WaydroidApplicationListModel;
class WaydroidApplicationDBusObject;
/**
* This class provides an interface to interact with the Waydroid container,
@ -23,26 +23,15 @@ class WaydroidApplicationListModel;
*
* @author Florian RICHER <florian.richer@protonmail.com>
*/
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<void> initialize(const SystemType systemType, const RomType romType, const bool forced = false);
Q_INVOKABLE QCoro::QmlTask startSessionQml();
QCoro::Task<void> startSession();
Q_INVOKABLE QCoro::QmlTask stopSessionQml();
QCoro::Task<void> stopSession();
Q_INVOKABLE QCoro::QmlTask resetWaydroidQml();
QCoro::Task<void> 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<QDBusObjectPath> 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<WaydroidApplicationDBusObject::Ptr> m_applicationObjects;
};

View file

@ -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

View file

@ -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
}
}

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
* 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
}
}

View file

@ -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")
}
}

View file

@ -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)
}
}

View file

@ -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 })
}
}
}

View file

@ -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")
}
}
}