shift-shell/components/waydroidintegrationplugin/waydroidstate.cpp
Florian RICHER 14839c4389 kcm: Implement minimal Waydroid support
I prefer create MR now to avoid biggest merge request.

It add minimalist implementation: 
- First configuration of Waydroid
- Stop / Start Waydroid session
- Configure properties of Waydroid
- Display current ip of Waydroid

| Not installed | First initialization | initializing | SessionNotStarted | SessionStarting | SessionStarted |
| ------ | ------ | ------ | ------ | ------ | ------ |
| ![Copie_d_écran_20250705_162112](/uploads/66844057ee6c955803288993809616fa/Copie_d_écran_20250705_162112.png) | ![Copie_d_écran_20250707_234822](/uploads/133779f8d4a70551a321938a7193aa3d/Copie_d_écran_20250707_234822.png) | ![Copie_d_écran_20250707_234829](/uploads/bacecd42875e3afd48dba2f9472b0f13/Copie_d_écran_20250707_234829.png) | ![Copie_d_écran_20250707_234908](/uploads/c6eff9833e33f30797088e327fcf6ea3/Copie_d_écran_20250707_234908.png) | ![Copie_d_écran_20250707_234919](/uploads/1927e2334d7e3b5790e5fab9037feff7/Copie_d_écran_20250707_234919.png) | ![Copie_d_écran_20250707_235239](/uploads/9be45b0a7b5f988de0e9ac297a2447c8/Copie_d_écran_20250707_235239.png) |

Linked to https://invent.kde.org/teams/plasma-mobile/issues/-/issues/307

**Note for Reviewer**: In my local environment, i need to add manually the KAuth files otherwise polkit not recognize the implementation.

```
sudo cp ~/kde/usr/share/dbus-1/system-services/org.kde.plasma.mobileshell.waydroidhelper.service /usr/share/dbus-1/system-services/
sudo cp ~/kde/usr/share/dbus-1/system.d/org.kde.plasma.mobileshell.waydroidhelper.conf /usr/share/dbus-1/system.d/
sudo cp ~/kde/usr/share/polkit-1/actions/org.kde.plasma.mobileshell.waydroidhelper.policy /usr/share/polkit-1/actions/
```
2025-07-10 12:00:41 -04:00

399 lines
10 KiB
C++

/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "waydroidstate.h"
#include "waydroidintegrationplugin_debug.h"
#include <QClipboard>
#include <QDebug>
#include <QGuiApplication>
#include <QProcess>
#include <QRegularExpression>
#include <QTimer>
#include <QtLogging>
#include <KAuth/Action>
#include <KAuth/ExecuteJob>
#include <KLocalizedString>
using namespace Qt::StringLiterals;
#define WAYDROID_COMMAND "waydroid"
#define MULTI_WINDOWS_PROP_KEY "persist.waydroid.multi_windows"
#define SUSPEND_PROP_KEY "persist.waydroid.suspend"
#define UEVENT_PROP_KEY "persist.waydroid.uevent"
static const QRegularExpression sessionRegExp(u"Session:\\s*(\\w+)"_s);
static const QRegularExpression ipAdressRegExp(u"IP address:\\s*(\\d+\\.\\d+\\.\\d+\\.\\d+)"_s);
WaydroidState::WaydroidState(QObject *parent)
: QObject{parent}
{
// Connect it-self to auto-refresh when required status has changed
connect(this, &WaydroidState::statusChanged, this, &WaydroidState::refreshSessionInfo);
connect(this, &WaydroidState::sessionStatusChanged, this, &WaydroidState::refreshPropsInfo);
refreshSupportsInfo();
}
void WaydroidState::refreshSupportsInfo()
{
const QStringList arguments{u"-h"_s};
QProcess *process = new QProcess(this);
process->start(WAYDROID_COMMAND, arguments);
process->waitForFinished();
const int exitCode = process->exitCode();
if (exitCode != 0) {
m_status = NotSupported;
Q_EMIT statusChanged();
return;
}
const QString output = fetchSessionInfo();
if (!output.contains("WayDroid is not initialized")) {
m_status = Initialized;
} else {
m_status = NotInitialized;
}
Q_EMIT statusChanged();
}
void WaydroidState::refreshSessionInfo()
{
if (m_status != Initialized) {
return;
}
const QString output = fetchSessionInfo();
const QString sessionMatchResult = extractRegExp(output, sessionRegExp);
WaydroidState::SessionStatus newSessionStatus;
if (!sessionMatchResult.isEmpty()) {
newSessionStatus = sessionMatchResult.contains("RUNNING") ? SessionRunning : SessionStopped;
} else {
newSessionStatus = SessionStopped;
}
if (m_sessionStatus != newSessionStatus) {
m_sessionStatus = newSessionStatus;
Q_EMIT sessionStatusChanged();
}
m_ipAddress = extractRegExp(output, ipAdressRegExp);
Q_EMIT ipAddressChanged();
}
void WaydroidState::refreshPropsInfo()
{
if (m_sessionStatus != SessionRunning) {
return;
}
const QString multiWindowsPropValue = fetchPropValue(MULTI_WINDOWS_PROP_KEY, "false");
m_multiWindows = multiWindowsPropValue == "true";
Q_EMIT multiWindowsChanged();
const QString suspendPropValue = fetchPropValue(SUSPEND_PROP_KEY, "true");
m_suspend = suspendPropValue == "true";
Q_EMIT suspendChanged();
const QString ueventPropValue = fetchPropValue(UEVENT_PROP_KEY, "false");
m_uevent = ueventPropValue == "true";
Q_EMIT ueventChanged();
}
void WaydroidState::resetError()
{
m_errorTitle = "";
Q_EMIT errorTitleChanged();
if (m_errorMessage != "") {
m_errorMessage = "";
Q_EMIT errorMessageChanged();
}
}
void WaydroidState::initialize(const SystemType systemType, const RomType romType, const bool forced)
{
if (m_status == Initializing) {
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;
}
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::finished, this, [this](KJob *job, auto) {
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();
});
}
void WaydroidState::startSession()
{
if (m_sessionStatus == SessionStarting || m_sessionStatus == SessionRunning) {
return;
}
m_sessionStatus = SessionStarting;
Q_EMIT sessionStatusChanged();
const QStringList arguments{u"session"_s, u"start"_s};
// Don't wait for result because the command is blocking
QProcess *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);
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);
}
void WaydroidState::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();
if (process->exitCode() == 0) {
m_sessionStatus = SessionStopped;
Q_EMIT sessionStatusChanged();
} else {
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to stop the Waydroid session: " << process->readAllStandardError();
}
}
void WaydroidState::copyToClipboard(const QString text)
{
qGuiApp->clipboard()->setText(text);
}
WaydroidState::Status WaydroidState::status() const
{
return m_status;
}
WaydroidState::SessionStatus WaydroidState::sessionStatus() const
{
return m_sessionStatus;
}
QString WaydroidState::ipAddress() const
{
return m_ipAddress;
}
QString WaydroidState::errorTitle() const
{
return m_errorTitle;
}
QString WaydroidState::errorMessage() const
{
return m_errorMessage;
}
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)
{
const QStringList arguments{u"prop"_s, u"get"_s, key};
QProcess *process = new QProcess(this);
process->start(WAYDROID_COMMAND, arguments);
process->waitForFinished();
const QString commandOutput = process->readAllStandardOutput();
const QString value = commandOutput.split("\n").first().trimmed();
if (value.isEmpty()) {
return defaultValue;
}
return value;
}
bool WaydroidState::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();
return process->exitCode() == 0;
}
QString WaydroidState::extractRegExp(const QString text, const QRegularExpression regExp) const
{
const QRegularExpressionMatch match = regExp.match(text);
if (match.hasMatch() && match.lastCapturedIndex() > 0) {
return match.captured(match.lastCapturedIndex());
} else {
return "";
}
}
void WaydroidState::checkSessionStarting(const int limit, const int tried)
{
if (m_sessionStatus != SessionStarting) {
return;
}
const QString output = fetchSessionInfo();
const QString sessionMatchResult = extractRegExp(output, sessionRegExp);
if (sessionMatchResult.contains("RUNNING")) {
m_sessionStatus = SessionRunning;
Q_EMIT sessionStatusChanged();
} else if (tried == limit) {
m_sessionStatus = SessionStopped;
Q_EMIT sessionStatusChanged();
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "Failed to start the session after " << tried << " tries";
} else {
QTimer::singleShot(500, [this, tried, limit]() {
checkSessionStarting(limit, tried + 1);
});
}
}