Improve Waydroid app management

Handle unavailable Waydroid sessions more cleanly and add KCM controls for launching apps, Game Shell visibility, and compatibility overrides.
This commit is contained in:
Marco Allegretti 2026-04-24 09:35:28 +02:00
parent eb177e0394
commit a8e76002f4
10 changed files with 792 additions and 48 deletions

View file

@ -35,6 +35,17 @@ void WaydroidApplicationListModel::initializeApplications(const QList<QDBusObjec
endResetModel();
}
void WaydroidApplicationListModel::clearApplications()
{
if (m_applications.isEmpty()) {
return;
}
beginResetModel();
m_applications.clear();
endResetModel();
}
void WaydroidApplicationListModel::addApplication(const QDBusObjectPath &objectPath)
{
beginInsertRows({}, m_applications.size(), m_applications.size());

View file

@ -33,6 +33,7 @@ public:
QHash<int, QByteArray> roleNames() const override;
void initializeApplications(const QList<QDBusObjectPath> &applicationObjectPaths);
void clearApplications();
public Q_SLOTS:
void addApplication(const QDBusObjectPath &objectPath);

View file

@ -6,24 +6,38 @@
#include "waydroiddbusclient.h"
#include <KConfigGroup>
#include <QClipboard>
#include <QCoroDBusPendingReply>
#include <QDBusMessage>
#include <QGuiApplication>
#include <QTimer>
using namespace Qt::StringLiterals;
static const QString s_waydroidGamingGroup = QStringLiteral("WaydroidGaming");
static const QString s_gameShellPackagesKey = QStringLiteral("gameShellPackages");
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}}
, m_config{KSharedConfig::openConfig(QStringLiteral("plasmamobilerc"))}
{
m_configWatcher = KConfigWatcher::create(m_config);
connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group) {
if (group.name() == s_waydroidGamingGroup) {
m_config->reparseConfiguration();
reloadGameShellPackages();
}
});
reloadGameShellPackages();
// 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();
}
checkWaydroidObject();
}
connect(m_watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, [this](const QString &service, const QString &oldOwner, const QString &newOwner) {
@ -31,12 +45,11 @@ WaydroidDBusClient::WaydroidDBusClient(QObject *parent)
if (newOwner.isEmpty()) {
// Service stopped
m_connected = false;
m_connectionCheckPending = false;
resetState();
} else if (oldOwner.isEmpty()) {
// Service started
m_connected = true;
if (m_interface->isValid()) {
connectSignals();
}
checkWaydroidObject();
}
}
});
@ -44,26 +57,32 @@ WaydroidDBusClient::WaydroidDBusClient(QObject *parent)
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);
});
if (!m_signalsConnected) {
m_signalsConnected = true;
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::fakeTouchChanged, this, &WaydroidDBusClient::updateFakeTouch);
connect(m_interface, &OrgKdePlasmashellWaydroidInterface::fakeWifiChanged, this, &WaydroidDBusClient::updateFakeWifi);
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();
@ -74,6 +93,118 @@ void WaydroidDBusClient::connectSignals()
updateMultiWindows();
updateSuspend();
updateUevent();
updateFakeTouch();
updateFakeWifi();
}
void WaydroidDBusClient::checkWaydroidObject()
{
if (m_connectionCheckPending) {
return;
}
m_connectionCheckPending = true;
const QDBusMessage message =
QDBusMessage::createMethodCall(u"org.kde.plasmashell"_s, u"/Waydroid"_s, u"org.freedesktop.DBus.Introspectable"_s, u"Introspect"_s);
auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, &WaydroidDBusClient::onWaydroidObjectCheckFinished);
}
void WaydroidDBusClient::onWaydroidObjectCheckFinished(QDBusPendingCallWatcher *watcher)
{
m_connectionCheckPending = false;
QDBusPendingReply<QString> reply = *watcher;
if (!reply.isValid()) {
m_connected = false;
watcher->deleteLater();
resetState();
scheduleWaydroidObjectCheck();
return;
}
m_connected = true;
watcher->deleteLater();
if (m_interface->isValid()) {
connectSignals();
}
}
void WaydroidDBusClient::handleUnavailableReply()
{
if (!m_connected) {
return;
}
m_connected = false;
resetState();
scheduleWaydroidObjectCheck();
}
void WaydroidDBusClient::resetState()
{
if (m_status != NotSupported) {
m_status = NotSupported;
Q_EMIT statusChanged();
}
if (m_sessionStatus != SessionStopped) {
m_sessionStatus = SessionStopped;
Q_EMIT sessionStatusChanged();
}
if (m_systemType != UnknownSystemType) {
m_systemType = UnknownSystemType;
Q_EMIT systemTypeChanged();
}
if (!m_ipAddress.isEmpty()) {
m_ipAddress.clear();
Q_EMIT ipAddressChanged();
}
if (!m_androidId.isEmpty()) {
m_androidId.clear();
Q_EMIT androidIdChanged();
}
if (m_multiWindows) {
m_multiWindows = false;
Q_EMIT multiWindowsChanged();
}
if (m_suspend) {
m_suspend = false;
Q_EMIT suspendChanged();
}
if (m_uevent) {
m_uevent = false;
Q_EMIT ueventChanged();
}
if (!m_fakeTouch.isEmpty()) {
m_fakeTouch.clear();
Q_EMIT fakeTouchChanged();
}
if (!m_fakeWifi.isEmpty()) {
m_fakeWifi.clear();
Q_EMIT fakeWifiChanged();
}
m_applicationListModel->clearApplications();
}
void WaydroidDBusClient::scheduleWaydroidObjectCheck()
{
if (!QDBusConnection::sessionBus().interface()->isServiceRegistered(u"org.kde.plasmashell"_s)) {
return;
}
QTimer::singleShot(1000, this, &WaydroidDBusClient::checkWaydroidObject);
}
void WaydroidDBusClient::initializeApplicationListModel()
@ -85,6 +216,7 @@ void WaydroidDBusClient::initializeApplicationListModel()
QDBusPendingReply<QList<QDBusObjectPath>> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch applications:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -133,6 +265,10 @@ WaydroidApplicationListModel *WaydroidDBusClient::applicationListModel() const
QCoro::Task<void> WaydroidDBusClient::setMultiWindowsTask(const bool multiWindows)
{
if (!m_connected) {
co_return;
}
co_await m_interface->setMultiWindows(multiWindows);
}
@ -148,6 +284,10 @@ bool WaydroidDBusClient::multiWindows() const
QCoro::Task<void> WaydroidDBusClient::setSuspendTask(const bool suspend)
{
if (!m_connected) {
co_return;
}
co_await m_interface->setSuspend(suspend);
}
@ -163,6 +303,10 @@ bool WaydroidDBusClient::suspend() const
QCoro::Task<void> WaydroidDBusClient::setUeventTask(const bool uevent)
{
if (!m_connected) {
co_return;
}
co_await m_interface->setUevent(uevent);
}
@ -173,6 +317,10 @@ QCoro::QmlTask WaydroidDBusClient::setUevent(const bool uevent)
QCoro::Task<void> WaydroidDBusClient::refreshSessionInfoTask()
{
if (!m_connected) {
co_return;
}
co_await m_interface->refreshSessionInfo();
}
@ -183,6 +331,10 @@ QCoro::QmlTask WaydroidDBusClient::refreshSessionInfo()
QCoro::Task<void> WaydroidDBusClient::refreshAndroidIdTask()
{
if (!m_connected) {
co_return;
}
co_await m_interface->refreshAndroidId();
}
@ -193,6 +345,10 @@ QCoro::QmlTask WaydroidDBusClient::refreshAndroidId()
QCoro::Task<void> WaydroidDBusClient::refreshApplicationsTask()
{
if (!m_connected) {
co_return;
}
co_await m_interface->refreshApplications();
}
@ -201,13 +357,87 @@ QCoro::QmlTask WaydroidDBusClient::refreshApplications()
return refreshApplicationsTask();
}
bool WaydroidDBusClient::gameShellEnabledForPackage(const QString &packageName) const
{
return m_gameShellPackages.contains(packageName);
}
void WaydroidDBusClient::setGameShellEnabledForPackage(const QString &packageName, bool enabled)
{
QStringList packages = m_gameShellPackages;
packages.removeAll(packageName);
if (enabled) {
packages.append(packageName);
}
packages.removeDuplicates();
packages.sort();
if (packages == m_gameShellPackages) {
return;
}
KConfigGroup group(m_config, s_waydroidGamingGroup);
group.writeEntry(s_gameShellPackagesKey, packages, KConfigGroup::Notify);
m_config->sync();
m_gameShellPackages = packages;
Q_EMIT gameShellPackagesChanged();
}
bool WaydroidDBusClient::uevent() const
{
return m_uevent;
}
QCoro::Task<void> WaydroidDBusClient::setFakeTouchTask(const QString &fakeTouch)
{
if (!m_connected) {
co_return;
}
co_await m_interface->setFakeTouch(fakeTouch);
}
QCoro::QmlTask WaydroidDBusClient::setFakeTouch(const QString &fakeTouch)
{
return setFakeTouchTask(fakeTouch);
}
QString WaydroidDBusClient::fakeTouch() const
{
return m_fakeTouch;
}
QCoro::Task<void> WaydroidDBusClient::setFakeWifiTask(const QString &fakeWifi)
{
if (!m_connected) {
co_return;
}
co_await m_interface->setFakeWifi(fakeWifi);
}
QCoro::QmlTask WaydroidDBusClient::setFakeWifi(const QString &fakeWifi)
{
return setFakeWifiTask(fakeWifi);
}
QString WaydroidDBusClient::fakeWifi() const
{
return m_fakeWifi;
}
QStringList WaydroidDBusClient::gameShellPackages() const
{
return m_gameShellPackages;
}
QCoro::Task<void> WaydroidDBusClient::initializeTask(const SystemType systemType, const RomType romType, const bool forced)
{
if (!m_connected) {
co_return;
}
co_await m_interface->initialize(systemType, romType, forced);
}
@ -218,6 +448,10 @@ QCoro::QmlTask WaydroidDBusClient::initialize(const SystemType systemType, const
QCoro::Task<void> WaydroidDBusClient::startSessionTask()
{
if (!m_connected) {
co_return;
}
co_await m_interface->startSession();
}
@ -228,6 +462,10 @@ QCoro::QmlTask WaydroidDBusClient::startSession()
QCoro::Task<void> WaydroidDBusClient::stopSessionTask()
{
if (!m_connected) {
co_return;
}
co_await m_interface->stopSession();
}
@ -238,6 +476,10 @@ QCoro::QmlTask WaydroidDBusClient::stopSession()
QCoro::Task<void> WaydroidDBusClient::resetWaydroidTask()
{
if (!m_connected) {
co_return;
}
co_await m_interface->resetWaydroid();
}
@ -248,6 +490,10 @@ QCoro::QmlTask WaydroidDBusClient::resetWaydroid()
QCoro::Task<void> WaydroidDBusClient::installApkTask(const QString apkFile)
{
if (!m_connected) {
co_return;
}
co_await m_interface->installApk(apkFile);
}
@ -256,8 +502,26 @@ QCoro::QmlTask WaydroidDBusClient::installApk(const QString apkFile)
return installApkTask(apkFile);
}
QCoro::Task<void> WaydroidDBusClient::launchApplicationTask(const QString appId)
{
if (!m_connected) {
co_return;
}
co_await m_interface->launchApplication(appId);
}
QCoro::QmlTask WaydroidDBusClient::launchApplication(const QString appId)
{
return launchApplicationTask(appId);
}
QCoro::Task<void> WaydroidDBusClient::deleteApplicationTask(const QString appId)
{
if (!m_connected) {
co_return;
}
co_await m_interface->deleteApplication(appId);
}
@ -266,6 +530,20 @@ QCoro::QmlTask WaydroidDBusClient::deleteApplication(const QString appId)
return deleteApplicationTask(appId);
}
QCoro::Task<void> WaydroidDBusClient::refreshSupportsInfoTask()
{
if (!m_connected) {
co_return;
}
co_await m_interface->refreshSupportsInfo();
}
QCoro::QmlTask WaydroidDBusClient::refreshSupportsInfo()
{
return refreshSupportsInfoTask();
}
void WaydroidDBusClient::updateStatus()
{
auto reply = m_interface->status();
@ -275,6 +553,7 @@ void WaydroidDBusClient::updateStatus()
QDBusPendingReply<int> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch status:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -299,6 +578,7 @@ void WaydroidDBusClient::updateSessionStatus()
QDBusPendingReply<int> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch sessionStatus:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -323,6 +603,7 @@ void WaydroidDBusClient::updateSystemType()
QDBusPendingReply<int> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch systemType:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -347,6 +628,7 @@ void WaydroidDBusClient::updateIpAddress()
QDBusPendingReply<QString> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch ipAddress:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -371,6 +653,7 @@ void WaydroidDBusClient::updateAndroidId()
QDBusPendingReply<QString> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch androidId:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -395,6 +678,7 @@ void WaydroidDBusClient::updateMultiWindows()
QDBusPendingReply<bool> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch multiWindows:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -419,6 +703,7 @@ void WaydroidDBusClient::updateSuspend()
QDBusPendingReply<bool> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch suspend:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -443,6 +728,7 @@ void WaydroidDBusClient::updateUevent()
QDBusPendingReply<bool> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch uevent:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
@ -458,7 +744,72 @@ void WaydroidDBusClient::updateUevent()
});
}
void WaydroidDBusClient::updateFakeTouch()
{
auto reply = m_interface->fakeTouch();
auto watcher = new QDBusPendingCallWatcher(reply, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) {
QDBusPendingReply<QString> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch fakeTouch:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
const QString fakeTouch = reply.argumentAt<0>();
if (m_fakeTouch != fakeTouch) {
m_fakeTouch = fakeTouch;
Q_EMIT fakeTouchChanged();
}
watcher->deleteLater();
});
}
void WaydroidDBusClient::updateFakeWifi()
{
auto reply = m_interface->fakeWifi();
auto watcher = new QDBusPendingCallWatcher(reply, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) {
QDBusPendingReply<QString> reply = *watcher;
if (!reply.isValid()) {
qDebug() << "WaydroidDBusClient: Failed to fetch fakeWifi:" << reply.error().message();
handleUnavailableReply();
watcher->deleteLater();
return;
}
const QString fakeWifi = reply.argumentAt<0>();
if (m_fakeWifi != fakeWifi) {
m_fakeWifi = fakeWifi;
Q_EMIT fakeWifiChanged();
}
watcher->deleteLater();
});
}
void WaydroidDBusClient::copyToClipboard(const QString text)
{
qGuiApp->clipboard()->setText(text);
}
void WaydroidDBusClient::reloadGameShellPackages()
{
const KConfigGroup group(m_config, s_waydroidGamingGroup);
QStringList packages = group.readEntry(s_gameShellPackagesKey, QStringList{});
packages.removeDuplicates();
packages.sort();
if (m_gameShellPackages == packages) {
return;
}
m_gameShellPackages = packages;
Q_EMIT gameShellPackagesChanged();
}

View file

@ -10,8 +10,12 @@
#include "waydroidapplicationlistmodel.h"
#include "waydroiddbusobject.h"
#include <KConfigWatcher>
#include <KSharedConfig>
#include <QCoroCore>
#include <QCoroQmlTask>
#include <QDBusPendingCallWatcher>
#include <QDBusServiceWatcher>
#include <QObject>
#include <QString>
@ -32,6 +36,9 @@ class WaydroidDBusClient : public QObject
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(QString fakeTouch READ fakeTouch WRITE setFakeTouch NOTIFY fakeTouchChanged)
Q_PROPERTY(QString fakeWifi READ fakeWifi WRITE setFakeWifi NOTIFY fakeWifiChanged)
Q_PROPERTY(QStringList gameShellPackages READ gameShellPackages NOTIFY gameShellPackagesChanged)
Q_PROPERTY(WaydroidApplicationListModel *applicationListModel READ applicationListModel CONSTANT)
public:
@ -98,16 +105,25 @@ public:
QCoro::QmlTask setSuspend(const bool suspend);
[[nodiscard]] bool uevent() const;
QCoro::QmlTask setUevent(const bool uevent);
[[nodiscard]] QString fakeTouch() const;
QCoro::QmlTask setFakeTouch(const QString &fakeTouch);
[[nodiscard]] QString fakeWifi() const;
QCoro::QmlTask setFakeWifi(const QString &fakeWifi);
[[nodiscard]] QStringList gameShellPackages() const;
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 launchApplication(const QString appId);
Q_INVOKABLE QCoro::QmlTask deleteApplication(const QString appId);
Q_INVOKABLE QCoro::QmlTask refreshSupportsInfo();
Q_INVOKABLE QCoro::QmlTask refreshSessionInfo();
Q_INVOKABLE QCoro::QmlTask refreshAndroidId();
Q_INVOKABLE QCoro::QmlTask refreshApplications();
Q_INVOKABLE bool gameShellEnabledForPackage(const QString &packageName) const;
Q_INVOKABLE void setGameShellEnabledForPackage(const QString &packageName, bool enabled);
Q_INVOKABLE void copyToClipboard(const QString text);
@ -122,6 +138,9 @@ Q_SIGNALS:
void multiWindowsChanged();
void suspendChanged();
void ueventChanged();
void fakeTouchChanged();
void fakeWifiChanged();
void gameShellPackagesChanged();
void actionFinished(const QString message);
void actionFailed(const QString message);
@ -136,12 +155,14 @@ private Q_SLOTS:
void updateMultiWindows();
void updateSuspend();
void updateUevent();
void updateFakeTouch();
void updateFakeWifi();
private:
OrgKdePlasmashellWaydroidInterface *m_interface;
QDBusServiceWatcher *m_watcher;
Status m_status{NotInitialized};
Status m_status{NotSupported};
SessionStatus m_sessionStatus{SessionStopped};
SystemType m_systemType{UnknownSystemType};
QString m_ipAddress{""};
@ -152,22 +173,40 @@ private:
bool m_multiWindows{false};
bool m_suspend{false};
bool m_uevent{false};
QString m_fakeTouch;
QString m_fakeWifi;
KSharedConfig::Ptr m_config;
KConfigWatcher::Ptr m_configWatcher;
QStringList m_gameShellPackages;
bool m_connected{false};
bool m_signalsConnected{false};
bool m_connectionCheckPending{false};
void connectSignals();
void initializeApplicationListModel();
void checkWaydroidObject();
void handleUnavailableReply();
void onWaydroidObjectCheckFinished(QDBusPendingCallWatcher *watcher);
void resetState();
void scheduleWaydroidObjectCheck();
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> launchApplicationTask(const QString appId);
QCoro::Task<void> deleteApplicationTask(const QString appId);
QCoro::Task<void> refreshSupportsInfoTask();
QCoro::Task<void> setMultiWindowsTask(const bool multiWindows);
QCoro::Task<void> setSuspendTask(const bool suspend);
QCoro::Task<void> setUeventTask(const bool uevent);
QCoro::Task<void> setFakeTouchTask(const QString &fakeTouch);
QCoro::Task<void> setFakeWifiTask(const QString &fakeWifi);
QCoro::Task<void> refreshSessionInfoTask();
QCoro::Task<void> refreshAndroidIdTask();
QCoro::Task<void> refreshApplicationsTask();
void reloadGameShellPackages();
};

View file

@ -31,6 +31,8 @@ using namespace Qt::StringLiterals;
#define MULTI_WINDOWS_PROP_KEY "persist.waydroid.multi_windows"
#define SUSPEND_PROP_KEY "persist.waydroid.suspend"
#define UEVENT_PROP_KEY "persist.waydroid.uevent"
#define FAKE_TOUCH_PROP_KEY "persist.waydroid.fake_touch"
#define FAKE_WIFI_PROP_KEY "persist.waydroid.fake_wifi"
static const QRegularExpression sessionRegExp(u"Session:\\s*(\\w+)"_s);
static const QRegularExpression ipAddressRegExp(u"IP address:\\s*(\\d+\\.\\d+\\.\\d+\\.\\d+)"_s);
@ -179,10 +181,12 @@ void WaydroidDBusObject::stopSession()
connect(process, &QProcess::finished, this, [this, process](int exitCode, QProcess::ExitStatus exitStatus) {
Q_UNUSED(exitStatus);
const QByteArray errorLog = process->readAllStandardError();
process->deleteLater();
if (exitCode == 0) {
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to stop the Waydroid session: " << process->readAllStandardError();
if (exitCode != 0) {
Q_EMIT errorOccurred(i18n("Failed to stop the Waydroid session."), QString::fromUtf8(errorLog));
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to stop the Waydroid session: " << errorLog;
return;
}
@ -250,6 +254,27 @@ void WaydroidDBusObject::installApk(const QString apkFile)
process->start(WAYDROID_COMMAND, arguments);
}
void WaydroidDBusObject::launchApplication(const QString appId)
{
const QStringList arguments{u"app"_s, u"launch"_s, appId};
QProcess *process = new QProcess(this);
connect(process, &QProcess::finished, this, [this, appId, process](int exitCode, QProcess::ExitStatus exitStatus) {
const QByteArray errorLog = process->readAllStandardError();
process->deleteLater();
if (exitCode == 0 && exitStatus == QProcess::NormalExit) {
Q_EMIT actionFinished(i18n("Application has been launched"));
} else {
Q_EMIT actionFailed(i18n("Application launch failed"));
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Error occurred while launching " << appId << ": " << errorLog;
}
});
process->start(WAYDROID_COMMAND, arguments);
}
void WaydroidDBusObject::deleteApplication(const QString appId)
{
const QStringList arguments{u"app"_s, u"remove"_s, appId};
@ -375,6 +400,52 @@ void WaydroidDBusObject::setUevent(const bool uevent)
coro(this, value, uevent);
}
QString WaydroidDBusObject::fakeTouch() const
{
return m_fakeTouch;
}
void WaydroidDBusObject::setFakeTouch(const QString &fakeTouch)
{
if (m_fakeTouch == fakeTouch) {
return;
}
auto coro = [](WaydroidDBusObject *self, QString fakeTouch) -> QCoro::Task<void> {
QPointer<WaydroidDBusObject> guard(self);
if (co_await self->writePropValue(FAKE_TOUCH_PROP_KEY, fakeTouch)) {
if (guard) {
self->m_fakeTouch = fakeTouch;
Q_EMIT self->fakeTouchChanged();
}
}
};
coro(this, fakeTouch);
}
QString WaydroidDBusObject::fakeWifi() const
{
return m_fakeWifi;
}
void WaydroidDBusObject::setFakeWifi(const QString &fakeWifi)
{
if (m_fakeWifi == fakeWifi) {
return;
}
auto coro = [](WaydroidDBusObject *self, QString fakeWifi) -> QCoro::Task<void> {
QPointer<WaydroidDBusObject> guard(self);
if (co_await self->writePropValue(FAKE_WIFI_PROP_KEY, fakeWifi)) {
if (guard) {
self->m_fakeWifi = fakeWifi;
Q_EMIT self->fakeWifiChanged();
}
}
};
coro(this, fakeWifi);
}
QList<QDBusObjectPath> WaydroidDBusObject::applications() const
{
QList<QDBusObjectPath> paths;
@ -513,6 +584,7 @@ void WaydroidDBusObject::refreshAndroidId()
}
} else {
m_androidId = "";
Q_EMIT actionFailed(i18n("Failed to fetch Android ID"));
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "KAuth returned an error code:" << executeJob->error();
}
@ -539,6 +611,12 @@ QCoro::Task<void> WaydroidDBusObject::refreshPropsInfo()
const QString ueventPropValue = co_await fetchPropValue(UEVENT_PROP_KEY, "false");
m_uevent = ueventPropValue == "true";
Q_EMIT ueventChanged();
m_fakeTouch = co_await fetchPropValue(FAKE_TOUCH_PROP_KEY, QString{});
Q_EMIT fakeTouchChanged();
m_fakeWifi = co_await fetchPropValue(FAKE_WIFI_PROP_KEY, QString{});
Q_EMIT fakeWifiChanged();
}
QCoro::Task<QString> WaydroidDBusObject::fetchPropValue(const QString key, const QString defaultValue)

View file

@ -96,6 +96,8 @@ Q_SIGNALS:
Q_SCRIPTABLE void multiWindowsChanged();
Q_SCRIPTABLE void suspendChanged();
Q_SCRIPTABLE void ueventChanged();
Q_SCRIPTABLE void fakeTouchChanged();
Q_SCRIPTABLE void fakeWifiChanged();
Q_SCRIPTABLE void applicationAdded(QDBusObjectPath path);
Q_SCRIPTABLE void applicationRemoved(QDBusObjectPath path);
@ -119,6 +121,10 @@ public Q_SLOTS:
Q_SCRIPTABLE void setSuspend(const bool suspend);
Q_SCRIPTABLE bool uevent() const;
Q_SCRIPTABLE void setUevent(const bool uevent);
Q_SCRIPTABLE QString fakeTouch() const;
Q_SCRIPTABLE void setFakeTouch(const QString &fakeTouch);
Q_SCRIPTABLE QString fakeWifi() const;
Q_SCRIPTABLE void setFakeWifi(const QString &fakeWifi);
Q_SCRIPTABLE QList<QDBusObjectPath> applications() const;
Q_SCRIPTABLE void initialize(const int systemType, const int romType, const bool forced = false);
@ -126,7 +132,9 @@ public Q_SLOTS:
Q_SCRIPTABLE void stopSession();
Q_SCRIPTABLE void resetWaydroid();
Q_SCRIPTABLE void installApk(const QString apkFile);
Q_SCRIPTABLE void launchApplication(const QString appId);
Q_SCRIPTABLE void deleteApplication(const QString appId);
Q_SCRIPTABLE void refreshSupportsInfo();
Q_SCRIPTABLE void refreshSessionInfo();
Q_SCRIPTABLE void refreshAndroidId();
Q_SCRIPTABLE void refreshApplications();
@ -143,8 +151,9 @@ private:
bool m_multiWindows{false};
bool m_suspend{false};
bool m_uevent{false};
QString m_fakeTouch;
QString m_fakeWifi;
void refreshSupportsInfo();
void refreshInstallationInfo();
QCoro::Task<void> refreshPropsInfo();

View file

@ -17,6 +17,45 @@ import org.kde.plasma.private.mobileshell.waydroidintegrationplugin as AIP
KCM.SimpleKCM {
id: root
function packagePatterns(value: string): var {
if (value === "") {
return []
}
return value.split(",")
.map(pattern => pattern.trim())
.filter(pattern => pattern !== "")
}
function wildcardRegExp(pattern: string): RegExp {
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
return new RegExp("^" + escaped.replace(/\*/g, ".*") + "$")
}
function hasExactPackage(value: string, packageName: string): bool {
return packagePatterns(value).includes(packageName)
}
function hasWildcardPackage(value: string, packageName: string): bool {
return packagePatterns(value)
.filter(pattern => pattern.includes("*"))
.some(pattern => wildcardRegExp(pattern).test(packageName))
}
function hasEffectivePackage(value: string, packageName: string): bool {
return hasExactPackage(value, packageName) || hasWildcardPackage(value, packageName)
}
function updateExactPackage(value: string, packageName: string, enabled: bool): string {
const updatedPatterns = packagePatterns(value).filter(pattern => pattern !== packageName)
if (enabled) {
updatedPatterns.push(packageName)
}
return updatedPatterns.join(",")
}
topPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
leftPadding: 0
@ -74,6 +113,7 @@ KCM.SimpleKCM {
}
ColumnLayout {
visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized
spacing: Kirigami.Units.largeSpacing
Kirigami.InlineMessage {
@ -85,6 +125,11 @@ KCM.SimpleKCM {
showCloseButton: true
}
Kirigami.PlaceholderMessage {
Layout.fillWidth: true
explanation: i18n("This page manages the launchers exported by Waydroid. Enable Show in Game Shell for Android apps you want listed in Game Center's Waydroid tab. Touch and Wi-Fi toggles add or remove exact package names from Waydroid's documented compatibility property lists. Wildcard rules remain in the main Waydroid properties page.")
}
FormCard.FormCard {
Repeater {
model: AIP.WaydroidDBusClient.applicationListModel
@ -95,23 +140,105 @@ KCM.SimpleKCM {
width: ListView.view.width
background: null
contentItem: RowLayout {
QQC2.Label {
contentItem: ColumnLayout {
spacing: Kirigami.Units.smallSpacing
RowLayout {
Layout.fillWidth: true
text: model.name
elide: Text.ElideRight
QQC2.Label {
Layout.fillWidth: true
text: model.name
elide: Text.ElideRight
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Launch the application")
icon.name: "media-playback-start"
onClicked: AIP.WaydroidDBusClient.launchApplication(model.id)
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Delete the application")
icon.name: "usermenu-delete"
onClicked: AIP.WaydroidDBusClient.deleteApplication(model.id)
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Delete the application")
icon.name: "usermenu-delete"
RowLayout {
Layout.fillWidth: true
onClicked: AIP.WaydroidDBusClient.deleteApplication(model.id)
QQC2.CheckBox {
text: i18n("Show in Game Shell")
checked: AIP.WaydroidDBusClient.gameShellPackages.indexOf(model.id) !== -1
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: AIP.WaydroidDBusClient.setGameShellEnabledForPackage(model.id, checked)
}
Item {
Layout.fillWidth: true
}
}
QQC2.Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
color: Kirigami.Theme.disabledTextColor
text: i18n("Adds this app to the Game Shell allowlist so it appears in Game Center under Waydroid.")
}
RowLayout {
Layout.fillWidth: true
QQC2.CheckBox {
id: fakeTouchToggle
text: i18n("Touch")
checked: root.hasEffectivePackage(AIP.WaydroidDBusClient.fakeTouch, model.id)
enabled: !root.hasWildcardPackage(AIP.WaydroidDBusClient.fakeTouch, model.id)
onClicked: {
AIP.WaydroidDBusClient.fakeTouch = root.updateExactPackage(AIP.WaydroidDBusClient.fakeTouch, model.id, checked)
}
QQC2.ToolTip.visible: hovered && !enabled
QQC2.ToolTip.text: i18n("Managed by a wildcard pattern in Waydroid properties")
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.CheckBox {
id: fakeWifiToggle
text: i18n("Wi-Fi")
checked: root.hasEffectivePackage(AIP.WaydroidDBusClient.fakeWifi, model.id)
enabled: !root.hasWildcardPackage(AIP.WaydroidDBusClient.fakeWifi, model.id)
onClicked: {
AIP.WaydroidDBusClient.fakeWifi = root.updateExactPackage(AIP.WaydroidDBusClient.fakeWifi, model.id, checked)
}
QQC2.ToolTip.visible: hovered && !enabled
QQC2.ToolTip.text: i18n("Managed by a wildcard pattern in Waydroid properties")
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
QQC2.Label {
Layout.fillWidth: true
visible: !fakeTouchToggle.enabled || !fakeWifiToggle.enabled
wrapMode: Text.WordWrap
color: Kirigami.Theme.disabledTextColor
text: i18n("One or more compatibility settings for this app come from a wildcard rule. Edit the global Waydroid property to change that rule.")
}
}
}
@ -121,4 +248,22 @@ KCM.SimpleKCM {
}
}
}
ColumnLayout {
visible: AIP.WaydroidDBusClient.status !== AIP.WaydroidDBusClient.Initialized
anchors.centerIn: parent
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
text: i18n("Waydroid is unavailable")
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
}
PC3.Button {
text: i18n("Check again")
Layout.alignment: Qt.AlignHCenter
onClicked: AIP.WaydroidDBusClient.refreshSupportsInfo()
}
}
}

View file

@ -15,6 +15,13 @@ import org.kde.plasma.private.mobileshell.waydroidintegrationplugin as AIP
ColumnLayout {
id: root
visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized
&& AIP.WaydroidDBusClient.sessionStatus === AIP.WaydroidDBusClient.SessionRunning
function packagePatternSummary(value: string): string {
return value === "" ? i18n("Not set") : value
}
FormCard.FormHeader {
title: i18n("General information")
}
@ -66,6 +73,64 @@ ColumnLayout {
onAccepted: AIP.WaydroidDBusClient.resetWaydroid()
}
Kirigami.PromptDialog {
id: fakeTouchDialog
title: i18n("Touch input override")
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
onOpened: {
fakeTouchField.text = AIP.WaydroidDBusClient.fakeTouch
fakeTouchField.forceActiveFocus()
}
onAccepted: AIP.WaydroidDBusClient.fakeTouch = fakeTouchField.text.trim()
ColumnLayout {
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: i18n("Comma-separated package names for apps where mouse input should be interpreted as touch. Supports * wildcards. Leave empty to clear the override.")
}
QQC2.TextField {
id: fakeTouchField
Layout.fillWidth: true
placeholderText: "com.rovio.*"
}
}
}
Kirigami.PromptDialog {
id: fakeWifiDialog
title: i18n("Wi-Fi override")
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
onOpened: {
fakeWifiField.text = AIP.WaydroidDBusClient.fakeWifi
fakeWifiField.forceActiveFocus()
}
onAccepted: AIP.WaydroidDBusClient.fakeWifi = fakeWifiField.text.trim()
ColumnLayout {
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: i18n("Comma-separated package names for apps that should always appear to be on Wi-Fi. Supports * wildcards. Leave empty to clear the override.")
}
QQC2.TextField {
id: fakeWifiField
Layout.fillWidth: true
placeholderText: "com.gameloft.*"
}
}
}
}
// Some information such as IP address can take time to be set by Waydroid
@ -140,5 +205,29 @@ ColumnLayout {
infoMessage.visible = true
}
}
FormCard.FormDelegateSeparator { above: uevent; below: fakeTouch }
FormCard.FormTextDelegate {
id: fakeTouch
text: i18n("Touch input override")
description: root.packagePatternSummary(AIP.WaydroidDBusClient.fakeTouch)
trailing: PC3.Button {
text: i18n("Edit")
onClicked: fakeTouchDialog.open()
}
}
FormCard.FormDelegateSeparator { above: fakeTouch; below: fakeWifi }
FormCard.FormTextDelegate {
id: fakeWifi
text: i18n("Wi-Fi override")
description: root.packagePatternSummary(AIP.WaydroidDBusClient.fakeWifi)
trailing: PC3.Button {
text: i18n("Edit")
onClicked: fakeWifiDialog.open()
}
}
}
}

View file

@ -29,7 +29,9 @@ KCM.SimpleKCM {
}
WaydroidLoader {
visible: AIP.WaydroidDBusClient.androidId === ""
visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized
&& AIP.WaydroidDBusClient.androidId === ""
&& !inlineMessage.visible
text: i18n("Fetching your Android ID.\nIt can take a few seconds.")
}
@ -53,7 +55,8 @@ KCM.SimpleKCM {
}
ColumnLayout {
visible: AIP.WaydroidDBusClient.androidId !== ""
visible: AIP.WaydroidDBusClient.status === AIP.WaydroidDBusClient.Initialized
&& AIP.WaydroidDBusClient.androidId !== ""
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent
anchors.leftMargin: Kirigami.Units.largeSpacing
@ -76,4 +79,22 @@ KCM.SimpleKCM {
}
}
}
ColumnLayout {
visible: AIP.WaydroidDBusClient.status !== AIP.WaydroidDBusClient.Initialized
anchors.centerIn: parent
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
text: i18n("Waydroid is unavailable")
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
}
QQC2.Button {
text: i18n("Check again")
Layout.alignment: Qt.AlignHCenter
onClicked: AIP.WaydroidDBusClient.refreshSupportsInfo()
}
}
}

View file

@ -29,14 +29,14 @@ KCM.SimpleKCM {
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
text: i18n("Waydroid is not installed")
text: i18n("Waydroid is unavailable")
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
}
PC3.Button {
text: i18n("Check installation")
text: i18n("Check again")
Layout.alignment: Qt.AlignHCenter
onClicked: AIP.WaydroidDBusClient.refreshSupportsInfo()
}