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/
```
This commit is contained in:
Florian RICHER 2025-07-10 18:00:41 +02:00 committed by Devin Lin
parent 036b633f16
commit 14839c4389
19 changed files with 1100 additions and 0 deletions

View file

@ -5,6 +5,7 @@ Dependencies:
- 'on': ['@all']
'require':
'frameworks/extra-cmake-modules': '@latest-kf6'
'frameworks/kauth': '@latest-kf6'
'frameworks/kconfig': '@latest-kf6'
'frameworks/kcoreaddons': '@latest-kf6'
'frameworks/ki18n': '@latest-kf6'

View file

@ -36,6 +36,7 @@ include(ECMFindQmlModule)
include(ECMGenerateQmlTypes)
include(ECMConfiguredInstall)
include(ECMFindQmlModule)
include(ECMQtDeclareLoggingCategory)
include(GenerateExportHeader)
include(KDEGitCommitHooks)
include(KDEClangFormat)
@ -119,6 +120,12 @@ set_package_properties(KWinDBusInterface PROPERTIES DESCRIPTION "KWin DBus inter
PURPOSE "Needed for virtual keyboard toggle button"
)
find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS Auth)
set_package_properties(KF6::AuthCore PROPERTIES
TYPE REQUIRED
PURPOSE "Allows Plasma Mobile to configure Waydroid"
)
include(CheckIncludeFiles)
ecm_find_qmlmodule(org.kde.pipewire 0.1)

View file

@ -0,0 +1,9 @@
Copyright (c) <year> <owner>
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: 2017-2021 Marco Martin <mart@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later
add_subdirectory(waydroidintegrationplugin)
add_subdirectory(hapticsplugin)
add_subdirectory(mmplugin)
add_subdirectory(mobileshell)

View file

@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
# SPDX-License-Identifier: BSD-2-Clause
ecm_add_qml_module(waydroidintegrationplugin URI org.kde.plasma.private.mobileshell.waydroidintegrationplugin GENERATE_PLUGIN_SOURCE)
target_sources(waydroidintegrationplugin PRIVATE waydroidstate.cpp)
target_link_libraries(waydroidintegrationplugin PRIVATE
Qt::Gui
Qt::Qml
Qt::Quick
KF6::AuthCore
KF6::I18n
)
ecm_finalize_qml_module(waydroidintegrationplugin)
ecm_qt_declare_logging_category(waydroidintegrationplugin
HEADER waydroidintegrationplugin_debug.h
IDENTIFIER WAYDROIDINTEGRATIONPLUGIN
DEFAULT_SEVERITY Warning
CATEGORY_NAME org.kde.plasma.private.mobileshell.waydroidintegrationplugin
DESCRIPTION "Plugin to add Waydroid Integration on Plasma"
)
add_subdirectory(kauth)

View file

@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
# SPDX-License-Identifier: BSD-2-Clause
add_executable(waydroidhelper)
target_sources(waydroidhelper PRIVATE waydroidhelper.cpp)
target_link_libraries(waydroidhelper
Qt6::Core
KF6::AuthCore
KF6::CoreAddons
)
install(TARGETS waydroidhelper DESTINATION ${KAUTH_HELPER_INSTALL_DIR})
find_package(KF6Auth NO_MODULE)
kauth_install_helper_files(waydroidhelper org.kde.plasma.mobileshell.waydroidhelper root)
kauth_install_actions(org.kde.plasma.mobileshell.waydroidhelper waydroidhelper.actions)
ecm_qt_declare_logging_category(waydroidhelper
HEADER waydroidhelper_debug.h
IDENTIFIER WAYDROIDHELPER
DEFAULT_SEVERITY Warning
CATEGORY_NAME org.kde.plasma.mobileshell.waydroidhelper
DESCRIPTION "Helper for Waydroid for some actions need root access"
)

View file

@ -0,0 +1,13 @@
# SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
# SPDX-License-Identifier: GPL-2.0-or-later
[Domain]
Name=Waydroid Management
Icon=color
[org.kde.plasma.mobileshell.waydroidhelper.initialize]
Name=Initialize Waydroid
Description=Allow initialization of Waydroid
Policy=yes
PolicyInactive=yes
Persistence=session

View file

@ -0,0 +1,61 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "waydroidhelper_debug.h"
#include <KAuth/ActionReply>
#include <KAuth/HelperSupport>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QLoggingCategory>
#include <QObject>
#include <QProcess>
using namespace Qt::StringLiterals;
#define WAYDROID_COMMAND "waydroid"
class WaydroidHelper : public QObject
{
Q_OBJECT
public Q_SLOTS:
KAuth::ActionReply initialize(const QVariantMap &args);
};
KAuth::ActionReply WaydroidHelper::initialize(const QVariantMap &args)
{
const QString systemType = args.value("systemType"_L1).toString();
const QString romType = args.value("romType"_L1).toString();
const bool forced = args.value("forced"_L1, false).toBool();
QStringList arguments{u"init"_s, u"-s"_s, systemType, u"-r"_s, romType};
if (forced) {
arguments << "-f";
}
QProcess *process = new QProcess(this);
process->start(WAYDROID_COMMAND, arguments);
process->waitForFinished();
if (process->exitCode() == 0) {
return KAuth::ActionReply::SuccessReply();
} else {
QByteArray errorData = process->readAllStandardError();
QString errorString = QString::fromUtf8(errorData);
qCWarning(WAYDROIDHELPER) << "Failed to initialize Waydroid: " << errorString;
KAuth::ActionReply reply = KAuth::ActionReply::HelperErrorReply();
reply.setErrorDescription(errorString);
return reply;
}
}
KAUTH_HELPER_MAIN("org.kde.plasma.mobileshell.waydroidhelper", WaydroidHelper)
#include "waydroidhelper.moc"

View file

@ -0,0 +1,399 @@
/*
* 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);
});
}
}

View file

@ -0,0 +1,178 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QObject>
#include <qqmlregistration.h>
#include <qtmetamacros.h>
/**
* This class provides an interface to interact with the Waydroid container,
* including session management and property configuration.
*
* @author Florian RICHER <florian.richer@protonmail.com>
*/
class WaydroidState : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(Status status READ status NOTIFY statusChanged)
Q_PROPERTY(SessionStatus sessionStatus READ sessionStatus NOTIFY sessionStatusChanged)
Q_PROPERTY(QString ipAddress READ ipAddress NOTIFY ipAddressChanged)
Q_PROPERTY(QString errorTitle READ errorTitle NOTIFY errorTitleChanged)
Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged)
Q_PROPERTY(bool multiWindows READ multiWindows WRITE setMultiWindows NOTIFY multiWindowsChanged)
Q_PROPERTY(bool suspend READ suspend WRITE setSuspend NOTIFY suspendChanged)
Q_PROPERTY(bool uevent READ uevent WRITE setUevent NOTIFY ueventChanged)
public:
WaydroidState(QObject *parent = nullptr);
/**
* @enum Status
* @brief Defines the possible installation statuses of the Waydroid service.
*/
enum Status {
NotSupported = 0,
NotInitialized,
Initializing,
Initialized
};
Q_ENUM(Status)
/**
* @enum SessionStatus
* @brief Defines the possible states of a Waydroid session.
*/
enum SessionStatus {
SessionStopped = 0,
SessionStarting,
SessionRunning
};
Q_ENUM(SessionStatus)
/**
* @enum SystemType
* @brief Defines the types of Android systems supported by Waydroid.
*/
enum SystemType {
Vanilla = 0, ///< Vanilla Android system.
Foss, ///< Free and Open Source Software variant.
Gapps ///< Variant with Google Apps included.
};
Q_ENUM(SystemType)
/**
* @enum RomType
* @brief Defines the types of ROMs supported by Waydroid.
*
* @todo Add OTA ROM with custom system url and vendor url
*/
enum RomType {
Lineage = 0, ///< LineageOS ROM.
Bliss ///< Bliss ROM.
};
Q_ENUM(RomType)
Q_INVOKABLE void refreshSupportsInfo();
Q_INVOKABLE void refreshSessionInfo();
Q_INVOKABLE void refreshPropsInfo();
Q_INVOKABLE void resetError();
Q_INVOKABLE void initialize(const SystemType systemType, const RomType romType, const bool forced = false);
Q_INVOKABLE void startSession();
Q_INVOKABLE void stopSession();
Q_INVOKABLE void copyToClipboard(const QString text);
Status status() const;
SessionStatus sessionStatus() const;
QString ipAddress() const;
QString errorTitle() const;
QString errorMessage() const;
bool multiWindows() const;
void setMultiWindows(const bool multiWindows);
bool suspend() const;
void setSuspend(const bool suspend);
bool uevent() const;
void setUevent(const bool uevent);
Q_SIGNALS:
void statusChanged();
void sessionStatusChanged();
void ipAddressChanged();
void multiWindowsChanged();
void suspendChanged();
void ueventChanged();
void errorTitleChanged();
void errorMessageChanged();
private:
Status m_status{NotInitialized};
SessionStatus m_sessionStatus{SessionStopped};
QString m_ipAddress{""};
QString m_errorTitle{""};
QString m_errorMessage{""};
// Waydroid props. See https://docs.waydro.id/usage/waydroid-prop-options
bool m_multiWindows{false};
bool m_suspend{false};
bool m_uevent{false};
/**
* @brief Executes the command to retrieve the current session status and related
* information from Waydroid.
*
* @return A QString containing the output of the Waydroid session status command.
*/
QString fetchSessionInfo();
/**
* @brief Executes the command to retrieve the value of a specified property from the Waydroid container.
*
* @param key The key of the property to fetch.
* @param defaultValue The default value to return if the property is not found or empty.
* @return A QString containing the property value, or the defaultValue if not found.
*/
QString fetchPropValue(const QString key, const QString defaultValue);
/**
* @brief Executes the command to writes a value to a specified property in the Waydroid container.
*
* @param key The key of the property to set.
* @param value The value to write to the property.
* @return A boolean indicating whether the write operation was successful.
*/
bool writePropValue(const QString key, const QString value);
/**
* @brief Extracts text from a string using a regular expression pattern.
*
* @param text The text to search within.
* @param regExp The regular expression pattern to use for extraction.
* @return A QString containing the extracted text if a match is found; otherwise, an empty string.
*/
QString extractRegExp(const QString text, const QRegularExpression regExp) const;
/**
* @brief Checks every 500ms if the session has started.
*
* This function periodically checks whether a session has started. If the session starts,
* it emits a "Running" signal. If the check count reaches the specified limit without
* the session starting, it emits a "Stopped" signal and logs a warning message.
*
* @param limit The maximum number of attempts to check for session start before stopping.
* @param tried The current number of attempts made to check for session start (defaults to 0).
*
* @todo Investigate using DBus for a cleaner implementation, potentially using the method:
* id.waydro.Container /ContainerManager id.waydro.ContainerManager.Start(a{ss} session).
* This would require duplicating the session start command logic from:
* https://github.com/waydroid/waydroid/blob/2c41162d8bfef5bf83333a6ce4834af0c3c2b535/tools/actions/session_manager.py#L31
*/
void checkSessionStarting(const int limit, const int tried = 0);
};

View file

@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later
add_subdirectory(waydroidintegration)
add_subdirectory(mobileshell)
add_subdirectory(info)
add_subdirectory(time)

View file

@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
# SPDX-License-Identifier: GPL-2.0-or-later
kcmutils_add_qml_kcm(kcm_waydroidintegration SOURCES waydroidintegration.cpp)
target_link_libraries(kcm_waydroidintegration PRIVATE
Qt::Core
KF6::CoreAddons
KF6::KCMUtils
KF6::I18n
KF6::KCMUtilsQuick
Plasma::Plasma
Plasma::PlasmaQuick
)

View file

@ -0,0 +1,8 @@
#! /usr/bin/env bash
# SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
# SPDX-License-Identifier: GPL-2.0-or-later
$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/kcm_waydroidintegration.pot

View file

@ -0,0 +1,25 @@
{
"KPlugin": {
"Authors": [
{
"Email": "florian.richer@protonmail.com",
"Name": "Florian RICHER"
}
],
"Category": "Waydroid Integration",
"Description": "Waydroid configuration",
"EnabledByDefault": true,
"FormFactors": [
"desktop",
"handset",
"tablet",
"mediacenter"
],
"Icon": "folder-android-symbolic",
"License": "GPL",
"Name": "Waydroid Integration",
"Website": "https://plasma-mobile.org/"
},
"X-KDE-Keywords": "waydroid",
"X-KDE-System-Settings-Parent-Category": "system-administration"
}

View file

@ -0,0 +1,117 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell.waydroidintegrationplugin as AIP
ColumnLayout {
id: root
FormCard.FormHeader {
title: i18n("General information")
}
FormCard.FormCard {
FormCard.FormTextDelegate {
text: i18n("IP address")
description: AIP.WaydroidState.ipAddress
trailing: PC3.Button {
text: i18n('Copy')
icon.name: 'edit-copy-symbolic'
onClicked: AIP.WaydroidState.copyToClipboard(AIP.WaydroidState.ipAddress)
}
}
FormCard.FormTextDelegate {
text: i18n("Waydroid status")
description: i18n("Running")
trailing: PC3.Button {
text: i18n("Stop session")
onClicked: AIP.WaydroidState.stopSession()
}
}
}
// Some informations as IP address can take time to be set by Waydroid
Timer {
id: autoRefreshSessionTimer
interval: 2000
repeat: true
running: root.visible
onTriggered: AIP.WaydroidState.refreshSessionInfo()
}
FormCard.FormHeader {
title: i18n("Waydroid properties")
}
FormCard.FormCard {
id: infoMessage
visible: false
Kirigami.Theme.inherit: false
Kirigami.Theme.backgroundColor: root.Kirigami.Theme.neutralBackgroundColor
FormCard.FormTextDelegate {
text: i18n("May require restarting the waydroid session to apply")
textItem.wrapMode: Text.WordWrap
icon.name: "dialog-warning"
}
}
Connections {
target: AIP.WaydroidState
function onSessionStatusChanged() {
infoMessage.visible = false
}
}
FormCard.FormCard {
FormCard.FormSwitchDelegate {
id: multiWindows
text: i18n("Multi Windows")
description: i18n("Enables/Disables window integration with the desktop")
checked: AIP.WaydroidState.multiWindows
onToggled: {
AIP.WaydroidState.multiWindows = checked
infoMessage.visible = true
}
}
FormCard.FormDelegateSeparator { above: multiWindows; below: suspend }
FormCard.FormSwitchDelegate {
id: suspend
text: i18n("Suspend")
description: i18n("Let the Waydroid container sleep (after the display timeout) when no apps are active")
checked: AIP.WaydroidState.suspend
onToggled: {
AIP.WaydroidState.suspend = checked
infoMessage.visible = true
}
}
FormCard.FormDelegateSeparator { above: suspend; below: uevent }
FormCard.FormSwitchDelegate {
id: uevent
text: i18n("UEvent")
description: i18n("Allow android direct access to hotplugged devices")
checked: AIP.WaydroidState.uevent
onToggled: {
AIP.WaydroidState.uevent = checked
infoMessage.visible = true
}
}
}
}

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell.waydroidintegrationplugin as AIP
ColumnLayout {
FormCard.FormHeader {
title: i18n("Initial configuration")
}
FormCard.FormCard {
FormCard.FormComboBoxDelegate {
id: systemType
text: i18n("System type")
model: [
{"name": "Vanilla", "value": AIP.WaydroidState.Vanilla},
{"name": "FOSS", "value": AIP.WaydroidState.Foss},
{"name": "GAPPS", "value": AIP.WaydroidState.Gapps}
]
textRole: "name"
valueRole: "value"
}
}
PC3.Button {
text: i18n("Configure waydroid")
Layout.alignment: Qt.AlignHCenter
enabled: systemType.currentValue !== undefined
onClicked: AIP.WaydroidState.initialize(systemType.currentValue, AIP.WaydroidState.Lineage)
}
}

View file

@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.19 as Kirigami
import org.kde.plasma.components 3.0 as PC3
ColumnLayout {
id: root
property string text
anchors.centerIn: parent
spacing: Kirigami.Units.largeSpacing
PC3.BusyIndicator {
Layout.alignment: Qt.AlignHCenter
implicitHeight: Kirigami.Units.iconSizes.huge
implicitWidth: Kirigami.Units.iconSizes.huge
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
}
QQC2.Label {
text: root.text
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
}

View file

@ -0,0 +1,110 @@
/*
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.19 as Kirigami
import org.kde.kcmutils as KCM
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell.waydroidintegrationplugin as AIP
KCM.SimpleKCM {
id: root
title: i18n("Waydroid Integration")
topPadding: 0
bottomPadding: 0
leftPadding: 0
rightPadding: 0
ColumnLayout {
visible: AIP.WaydroidState.errorTitle === "" && AIP.WaydroidState.status == AIP.WaydroidState.NotSupported
anchors.centerIn: parent
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
text: i18n("Waydroid is not installed")
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
}
PC3.Button {
text: i18n("Check installation")
Layout.alignment: Qt.AlignHCenter
onClicked: AIP.WaydroidState.refreshSupportsInfo()
}
}
WaydroidInitialConfigurationForm {
visible: AIP.WaydroidState.errorTitle === "" && AIP.WaydroidState.status == AIP.WaydroidState.NotInitialized
}
WaydroidLoader {
visible: AIP.WaydroidState.errorTitle === "" && AIP.WaydroidState.status == AIP.WaydroidState.Initializing
text: i18n("Waydroid is initializing.\nIt can take a few minutes.")
}
ColumnLayout {
visible: AIP.WaydroidState.errorTitle === "" && AIP.WaydroidState.status == AIP.WaydroidState.Initialized && AIP.WaydroidState.sessionStatus == AIP.WaydroidState.SessionStopped
anchors.centerIn: parent
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
text: i18n("The Waydroid session is not running.")
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
}
PC3.Button {
text: i18n("Start the session")
Layout.alignment: Qt.AlignHCenter
onClicked: AIP.WaydroidState.startSession()
}
}
WaydroidLoader {
visible: AIP.WaydroidState.errorTitle === "" && AIP.WaydroidState.status == AIP.WaydroidState.Initialized && AIP.WaydroidState.sessionStatus == AIP.WaydroidState.SessionStarting
text: i18n("Waydroid session is starting.\nIt can take a few seconds.")
}
WaydroidConfigurationForm {
visible: AIP.WaydroidState.errorTitle === "" && AIP.WaydroidState.status == AIP.WaydroidState.Initialized && AIP.WaydroidState.sessionStatus == AIP.WaydroidState.SessionRunning
}
ColumnLayout {
visible: AIP.WaydroidState.errorTitle !== ""
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent
anchors.leftMargin: Kirigami.Units.largeSpacing
anchors.right: parent
anchors.rightMargin: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
QQC2.Label {
text: AIP.WaydroidState.errorTitle
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
}
QQC2.TextArea {
visible: AIP.WaydroidState.errorMessage !== ""
text: AIP.WaydroidState.errorMessage
readOnly: true
wrapMode: TextEdit.Wrap
Layout.fillWidth: true
}
PC3.Button {
text: i18n("Go back")
Layout.alignment: Qt.AlignHCenter
onClicked: AIP.WaydroidState.resetError()
}
}
}

View file

@ -0,0 +1,29 @@
/**
* SPDX-FileCopyrightText: 2025 Florian RICHER <florian.richer@protonmail.com>
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <KPluginFactory>
#include <KConfigGroup>
#include <KQuickManagedConfigModule>
#include <KSharedConfig>
class KCMWaydroidIntegration : public KQuickManagedConfigModule
{
Q_OBJECT
public:
KCMWaydroidIntegration(QObject *parent, const KPluginMetaData &data)
: KQuickManagedConfigModule(parent, data)
{
setButtons({});
}
private:
KSharedConfig::Ptr m_config;
};
K_PLUGIN_CLASS_WITH_JSON(KCMWaydroidIntegration, "kcm_waydroidintegration.json")
#include "waydroidintegration.moc"