diff --git a/components/waydroidintegrationplugin/waydroidapplicationlistmodel.cpp b/components/waydroidintegrationplugin/waydroidapplicationlistmodel.cpp index 09acae2a..a203009e 100644 --- a/components/waydroidintegrationplugin/waydroidapplicationlistmodel.cpp +++ b/components/waydroidintegrationplugin/waydroidapplicationlistmodel.cpp @@ -35,6 +35,17 @@ void WaydroidApplicationListModel::initializeApplications(const QList roleNames() const override; void initializeApplications(const QList &applicationObjectPaths); + void clearApplications(); public Q_SLOTS: void addApplication(const QDBusObjectPath &objectPath); diff --git a/components/waydroidintegrationplugin/waydroiddbusclient.cpp b/components/waydroidintegrationplugin/waydroiddbusclient.cpp index 45f343c4..3dbe1185 100644 --- a/components/waydroidintegrationplugin/waydroiddbusclient.cpp +++ b/components/waydroidintegrationplugin/waydroiddbusclient.cpp @@ -6,24 +6,38 @@ #include "waydroiddbusclient.h" +#include + #include #include +#include #include +#include 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 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> 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 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 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 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 WaydroidDBusClient::refreshSessionInfoTask() { + if (!m_connected) { + co_return; + } + co_await m_interface->refreshSessionInfo(); } @@ -183,6 +331,10 @@ QCoro::QmlTask WaydroidDBusClient::refreshSessionInfo() QCoro::Task WaydroidDBusClient::refreshAndroidIdTask() { + if (!m_connected) { + co_return; + } + co_await m_interface->refreshAndroidId(); } @@ -193,6 +345,10 @@ QCoro::QmlTask WaydroidDBusClient::refreshAndroidId() QCoro::Task 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 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 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 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 WaydroidDBusClient::startSessionTask() { + if (!m_connected) { + co_return; + } + co_await m_interface->startSession(); } @@ -228,6 +462,10 @@ QCoro::QmlTask WaydroidDBusClient::startSession() QCoro::Task WaydroidDBusClient::stopSessionTask() { + if (!m_connected) { + co_return; + } + co_await m_interface->stopSession(); } @@ -238,6 +476,10 @@ QCoro::QmlTask WaydroidDBusClient::stopSession() QCoro::Task WaydroidDBusClient::resetWaydroidTask() { + if (!m_connected) { + co_return; + } + co_await m_interface->resetWaydroid(); } @@ -248,6 +490,10 @@ QCoro::QmlTask WaydroidDBusClient::resetWaydroid() QCoro::Task 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 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 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 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 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 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 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 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 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 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 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 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 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 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(); +} diff --git a/components/waydroidintegrationplugin/waydroiddbusclient.h b/components/waydroidintegrationplugin/waydroiddbusclient.h index 1b707936..7836f2f0 100644 --- a/components/waydroidintegrationplugin/waydroiddbusclient.h +++ b/components/waydroidintegrationplugin/waydroiddbusclient.h @@ -10,8 +10,12 @@ #include "waydroidapplicationlistmodel.h" #include "waydroiddbusobject.h" +#include +#include + #include #include +#include #include #include #include @@ -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 initializeTask(const SystemType systemType, const RomType romType, const bool forced = false); QCoro::Task startSessionTask(); QCoro::Task stopSessionTask(); QCoro::Task resetWaydroidTask(); QCoro::Task installApkTask(const QString apkFile); + QCoro::Task launchApplicationTask(const QString appId); QCoro::Task deleteApplicationTask(const QString appId); + QCoro::Task refreshSupportsInfoTask(); QCoro::Task setMultiWindowsTask(const bool multiWindows); QCoro::Task setSuspendTask(const bool suspend); QCoro::Task setUeventTask(const bool uevent); + QCoro::Task setFakeTouchTask(const QString &fakeTouch); + QCoro::Task setFakeWifiTask(const QString &fakeWifi); QCoro::Task refreshSessionInfoTask(); QCoro::Task refreshAndroidIdTask(); QCoro::Task refreshApplicationsTask(); + + void reloadGameShellPackages(); }; \ No newline at end of file diff --git a/components/waydroidintegrationplugin/waydroiddbusobject.cpp b/components/waydroidintegrationplugin/waydroiddbusobject.cpp index 17ae31f5..686580ac 100644 --- a/components/waydroidintegrationplugin/waydroiddbusobject.cpp +++ b/components/waydroidintegrationplugin/waydroiddbusobject.cpp @@ -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 { + QPointer 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 { + QPointer 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 WaydroidDBusObject::applications() const { QList 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 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 WaydroidDBusObject::fetchPropValue(const QString key, const QString defaultValue) diff --git a/components/waydroidintegrationplugin/waydroiddbusobject.h b/components/waydroidintegrationplugin/waydroiddbusobject.h index 82e2ba3c..c1c5d1fb 100644 --- a/components/waydroidintegrationplugin/waydroiddbusobject.h +++ b/components/waydroidintegrationplugin/waydroiddbusobject.h @@ -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 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 refreshPropsInfo(); diff --git a/kcms/waydroidintegration/ui/WaydroidApplicationsPage.qml b/kcms/waydroidintegration/ui/WaydroidApplicationsPage.qml index 7ee54b11..941df96e 100644 --- a/kcms/waydroidintegration/ui/WaydroidApplicationsPage.qml +++ b/kcms/waydroidintegration/ui/WaydroidApplicationsPage.qml @@ -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() + } + } } diff --git a/kcms/waydroidintegration/ui/WaydroidConfigurationForm.qml b/kcms/waydroidintegration/ui/WaydroidConfigurationForm.qml index 88b51696..1858084c 100644 --- a/kcms/waydroidintegration/ui/WaydroidConfigurationForm.qml +++ b/kcms/waydroidintegration/ui/WaydroidConfigurationForm.qml @@ -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() + } + } } } diff --git a/kcms/waydroidintegration/ui/WaydroidGooglePlayProtectConfigurationPage.qml b/kcms/waydroidintegration/ui/WaydroidGooglePlayProtectConfigurationPage.qml index 1087bcb4..89aa2472 100644 --- a/kcms/waydroidintegration/ui/WaydroidGooglePlayProtectConfigurationPage.qml +++ b/kcms/waydroidintegration/ui/WaydroidGooglePlayProtectConfigurationPage.qml @@ -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() + } + } } diff --git a/kcms/waydroidintegration/ui/main.qml b/kcms/waydroidintegration/ui/main.qml index 6e5b618a..432206c7 100644 --- a/kcms/waydroidintegration/ui/main.qml +++ b/kcms/waydroidintegration/ui/main.qml @@ -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() }