shift-shell/components/waydroidintegrationplugin/waydroidstate.cpp
2025-07-21 18:39:43 +02:00

473 lines
12 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 "waydroidshared.h"
#include <QClipboard>
#include <QDebug>
#include <QDir>
#include <QGuiApplication>
#include <QProcess>
#include <QRegularExpression>
#include <QTimer>
#include <QtLogging>
#include <KAuth/Action>
#include <KAuth/ExecuteJob>
#include <KLocalizedString>
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"
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)
: QObject{parent}
, m_applicationListModel{new WaydroidApplicationListModel{this}}
{
// Connect it-self to auto-refresh when required status has changed
connect(this, &WaydroidState::statusChanged, this, &WaydroidState::refreshSessionInfo);
connect(this, &WaydroidState::statusChanged, this, &WaydroidState::refreshInstallationInfo);
connect(this, &WaydroidState::sessionStatusChanged, this, &WaydroidState::refreshPropsInfo);
refreshSupportsInfo();
}
void WaydroidState::refreshSupportsInfo()
{
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::refreshInstallationInfo()
{
if (m_status != Initialized) {
return;
}
QFile file("/var/lib/waydroid/waydroid.cfg");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return;
}
QTextStream in(&file);
const QString fileContent = in.readAll();
const QString systemMatch = extractRegExp(fileContent, systemOtaRegExp);
if (systemMatch.contains("vanilla", Qt::CaseInsensitive)) {
m_systemType = Vanilla;
} else if (systemMatch.contains("gapps", Qt::CaseInsensitive)) {
m_systemType = Gapps;
} else if (systemMatch.contains("foss", Qt::CaseInsensitive)) {
m_systemType = Foss;
} else {
m_systemType = UnknownSystemType;
}
Q_EMIT systemTypeChanged();
}
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::refreshAndroidId()
{
if (m_status != Initialized) {
return;
}
KAuth::Action writeAction(u"org.kde.plasma.mobileshell.waydroidhelper.getandroidid"_s);
writeAction.setHelperId(u"org.kde.plasma.mobileshell.waydroidhelper"_s);
KAuth::ExecuteJob *job = writeAction.execute();
job->start();
connect(job, &KAuth::ExecuteJob::finished, this, [this](KJob *job, auto) {
KAuth::ExecuteJob *executeJob = dynamic_cast<KAuth::ExecuteJob *>(job);
if (executeJob->error() == 0) {
m_androidId = executeJob->data()["android_id"].toString();
} else {
m_androidId = "";
qCWarning(WAYDROIDINTEGRATIONPLUGIN) << "KAuth returned an error code:" << executeJob->error();
}
Q_EMIT androidIdChanged();
});
}
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;
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::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;
}
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)
{
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);
});
}
}