diff --git a/CMakeLists.txt b/CMakeLists.txt index 7161c4e4..d85a2db5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) boption(BLUETOOTH "Bluetooth" ON) +boption(NETWORK "Network" ON) include(cmake/install-qml-module.cmake) include(cmake/util.cmake) @@ -117,7 +118,7 @@ if (WAYLAND) list(APPEND QT_FPDEPS WaylandClient) endif() -if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH) +if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH OR NETWORK) set(DBUS ON) endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 52db00a5..c95ecf71 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,3 +33,7 @@ add_subdirectory(services) if (BLUETOOTH) add_subdirectory(bluetooth) endif() + +if (NETWORK) + add_subdirectory(network) +endif() diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt new file mode 100644 index 00000000..674241f1 --- /dev/null +++ b/src/network/CMakeLists.txt @@ -0,0 +1,95 @@ +set_source_files_properties(nm/org.freedesktop.NetworkManager.xml PROPERTIES + CLASSNAME DBusNetworkManagerProxy + NO_NAMESPACE TRUE + INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/nm/dbus_types.hpp +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.xml + nm/dbus_nm_backend +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.Device.xml PROPERTIES + CLASSNAME DBusNMDeviceProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.Device.xml + nm/dbus_nm_device +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.Device.Wireless.xml PROPERTIES + CLASSNAME DBusNMWirelessProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.Device.Wireless.xml + nm/dbus_nm_wireless +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.AccessPoint.xml PROPERTIES + CLASSNAME DBusNMAccessPointProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.AccessPoint.xml + nm/dbus_nm_accesspoint +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.Settings.Connection.xml PROPERTIES + CLASSNAME DBusNMConnectionSettingsProxy + NO_NAMESPACE TRUE + INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/nm/dbus_types.hpp +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.Settings.Connection.xml + nm/dbus_nm_connection_settings +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.Connection.Active.xml PROPERTIES + CLASSNAME DBusNMActiveConnectionProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.Connection.Active.xml + nm/dbus_nm_active_connection +) + +qt_add_library(quickshell-network STATIC + network.cpp + device.cpp + wifi.cpp + nm/backend.cpp + nm/device.cpp + nm/connection.cpp + nm/accesspoint.cpp + nm/wireless.cpp + nm/utils.cpp + nm/enums.hpp + ${NM_DBUS_INTERFACES} +) + +# dbus headers +target_include_directories(quickshell-network PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} +) + +qt_add_qml_module(quickshell-network + URI Quickshell.Network + VERSION 0.1 + DEPENDENCIES QtQml +) + +qs_add_module_deps_light(quickshell-network Quickshell) +install_qml_module(quickshell-network) + +target_link_libraries(quickshell-network PRIVATE Qt::Qml Qt::DBus) +qs_add_link_dependencies(quickshell-network quickshell-dbus) +target_link_libraries(quickshell PRIVATE quickshell-networkplugin) + +qs_module_pch(quickshell-network SET dbus) diff --git a/src/network/device.cpp b/src/network/device.cpp new file mode 100644 index 00000000..9aa1b239 --- /dev/null +++ b/src/network/device.cpp @@ -0,0 +1,53 @@ +#include "device.hpp" + +#include +#include + +#include "../core/logcat.hpp" + +namespace qs::network { + +namespace { +QS_LOGGING_CATEGORY(logNetworkDevice, "quickshell.network.device", QtWarningMsg); +} // namespace + +NetworkDevice::NetworkDevice(QObject* parent): QObject(parent) {}; + +void NetworkDevice::disconnect() { + if (this->bState == NetworkConnectionState::Disconnected) { + qCCritical(logNetworkDevice) << "Device" << this << "is already disconnected"; + return; + } + if (this->bState == NetworkConnectionState::Disconnecting) { + qCCritical(logNetworkDevice) << "Device" << this << "is already disconnecting"; + return; + } + qCDebug(logNetworkDevice) << "Disconnecting from device" << this; + this->requestDisconnect(); +} + +QString NetworkConnectionState::toString(NetworkConnectionState::Enum state) { + switch (state) { + case Unknown: return QStringLiteral("Unknown"); + case Connecting: return QStringLiteral("Connecting"); + case Connected: return QStringLiteral("Connected"); + case Disconnecting: return QStringLiteral("Disconnecting"); + case Disconnected: return QStringLiteral("Disconnected"); + default: return QStringLiteral("Unknown"); + } +} + +} // namespace qs::network + +QDebug operator<<(QDebug debug, const qs::network::NetworkDevice* device) { + auto saver = QDebugStateSaver(debug); + + if (device) { + debug.nospace() << "NetworkDevice(" << static_cast(device) + << ", name=" << device->name() << ")"; + } else { + debug << "BluetoothDevice(nullptr)"; + } + + return debug; +} diff --git a/src/network/device.hpp b/src/network/device.hpp new file mode 100644 index 00000000..99ef8e0a --- /dev/null +++ b/src/network/device.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include +#include + +#include "nm/enums.hpp" + +namespace qs::network { + +///! Connection state. +class NetworkConnectionState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Connecting = 1, + Connected = 2, + Disconnecting = 3, + Disconnected = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NetworkConnectionState::Enum state); +}; + +///! A Network device. +class NetworkDevice: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("Devices can only be acquired through Network"); + // clang-format off + /// The name of the device's control interface. + Q_PROPERTY(QString name READ name NOTIFY nameChanged BINDABLE bindableName); + /// The hardware address of the device in the XX:XX:XX:XX:XX:XX format. + Q_PROPERTY(QString address READ address NOTIFY addressChanged BINDABLE bindableAddress); + /// Connection state of the device. + Q_PROPERTY(qs::network::NetworkConnectionState::Enum state READ state NOTIFY stateChanged BINDABLE bindableState); + /// A more specific device state when the backend is NetworkManager. + Q_PROPERTY(qs::network::NMDeviceState::Enum nmState READ nmState NOTIFY nmStateChanged BINDABLE bindableNmState); + // clang-format on + +signals: + void nameChanged(); + void addressChanged(); + void stateChanged(); + void nmStateChanged(); + void requestDisconnect(); + +public: + explicit NetworkDevice(QObject* parent = nullptr); + + /// Disconnects the device and prevents it from automatically activating further connections. + Q_INVOKABLE void disconnect(); + + [[nodiscard]] QString name() const { return this->bName; }; + [[nodiscard]] QString address() const { return this->bAddress; }; + [[nodiscard]] NetworkConnectionState::Enum state() const { return this->bState; }; + [[nodiscard]] NMDeviceState::Enum nmState() const { return this->bNmState; }; + QBindable bindableName() { return &this->bName; }; + QBindable bindableAddress() { return &this->bAddress; }; + QBindable bindableState() { return &this->bState; }; + QBindable bindableNmState() { return &this->bNmState; }; + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bName, &NetworkDevice::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bAddress, &NetworkDevice::addressChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, NetworkConnectionState::Enum, bState, &NetworkDevice::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, NMDeviceState::Enum, bNmState, &NetworkDevice::nmStateChanged); + // clang-format on +}; + +} // namespace qs::network + +QDebug operator<<(QDebug debug, const qs::network::NetworkDevice* device); diff --git a/src/network/network.cpp b/src/network/network.cpp new file mode 100644 index 00000000..f7e3e49f --- /dev/null +++ b/src/network/network.cpp @@ -0,0 +1,35 @@ +#include "network.hpp" + +#include +#include + +#include "../core/logcat.hpp" +#include "nm/backend.hpp" + +namespace qs::network { + +namespace { +QS_LOGGING_CATEGORY(logNetwork, "quickshell.network", QtWarningMsg); +} // namespace + +Network::Network(QObject* parent): QObject(parent), mWifi(new Wifi(this)) { + // NetworkManager + auto* nm = new NetworkManager(this); + if (nm->isAvailable()) { + QObject::connect(nm, &NetworkManager::wifiDeviceAdded, this->wifi(), &Wifi::onDeviceAdded); + QObject::connect(nm, &NetworkManager::wifiDeviceRemoved, this->wifi(), &Wifi::onDeviceRemoved); + this->wifi()->bindableEnabled().setBinding([nm]() { return nm->wifiEnabled(); }); + this->wifi()->bindableHardwareEnabled().setBinding([nm]() { return nm->wifiHardwareEnabled(); } + ); + QObject::connect(this->wifi(), &Wifi::requestSetEnabled, nm, &NetworkManager::setWifiEnabled); + this->mBackend = nm; + this->mBackendType = NetworkBackendType::NetworkManager; + return; + } else { + delete nm; + } + + qCCritical(logNetwork) << "Network will not work. Could not find an available backend."; +} + +} // namespace qs::network diff --git a/src/network/network.hpp b/src/network/network.hpp new file mode 100644 index 00000000..9c93382b --- /dev/null +++ b/src/network/network.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include + +#include "wifi.hpp" + +namespace qs::network { + +///! The backend supplying the Network service. +class NetworkBackendType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + None = 0, + NetworkManager = 1, + }; + Q_ENUM(Enum); +}; + +class NetworkBackend: public QObject { + Q_OBJECT; + +public: + [[nodiscard]] virtual bool isAvailable() const = 0; + +protected: + explicit NetworkBackend(QObject* parent = nullptr): QObject(parent) {}; +}; + +///! The Network service. +/// An interface to a network backend (currently only NetworkManager), +/// which can be used to view, configure, and connect to various networks. +class Network: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(Network); + QML_SINGLETON; + + /// The wifi device service. + Q_PROPERTY(qs::network::Wifi* wifi READ wifi CONSTANT); + /// The backend being used to power the Network service. + Q_PROPERTY(qs::network::NetworkBackendType::Enum backend READ backend CONSTANT); + +public: + explicit Network(QObject* parent = nullptr); + + [[nodiscard]] Wifi* wifi() const { return this->mWifi; }; + [[nodiscard]] NetworkBackendType::Enum backend() const { return this->mBackendType; }; + +private: + Wifi* mWifi; + NetworkBackend* mBackend = nullptr; + NetworkBackendType::Enum mBackendType = NetworkBackendType::None; +}; + +} // namespace qs::network diff --git a/src/network/nm/accesspoint.cpp b/src/network/nm/accesspoint.cpp new file mode 100644 index 00000000..1cd7207c --- /dev/null +++ b/src/network/nm/accesspoint.cpp @@ -0,0 +1,68 @@ +#include "accesspoint.hpp" + +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "nm/dbus_nm_accesspoint.h" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMAccessPoint::NMAccessPoint(const QString& path, QObject* parent): QObject(parent) { + this->proxy = new DBusNMAccessPointProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for AP at" << path; + return; + } + + QObject::connect( + &this->accessPointProperties, + &DBusPropertyGroup::getAllFinished, + this, + &NMAccessPoint::ready, + Qt::SingleShotConnection + ); + + this->accessPointProperties.setInterface(this->proxy); + this->accessPointProperties.updateAllViaGetAll(); +} + +bool NMAccessPoint::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMAccessPoint::address() const { return this->proxy ? this->proxy->service() : QString(); } +QString NMAccessPoint::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/accesspoint.hpp b/src/network/nm/accesspoint.hpp new file mode 100644 index 00000000..a31e1be6 --- /dev/null +++ b/src/network/nm/accesspoint.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "enums.hpp" +#include "nm/dbus_nm_accesspoint.h" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211ApFlags::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211ApSecurityFlags::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211Mode::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +/// Proxy of a /org/freedesktop/NetworkManager/AccessPoint/* object. +class NMAccessPoint: public QObject { + Q_OBJECT; + +public: + explicit NMAccessPoint(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QByteArray ssid() const { return this->bSsid; }; + [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; }; + [[nodiscard]] NM80211ApFlags::Enum flags() const { return this->bFlags; }; + [[nodiscard]] NM80211ApSecurityFlags::Enum wpaFlags() const { return this->bWpaFlags; }; + [[nodiscard]] NM80211ApSecurityFlags::Enum rsnFlags() const { return this->bRsnFlags; }; + [[nodiscard]] NM80211Mode::Enum mode() const { return this->bMode; }; + +signals: + void ssidChanged(const QByteArray& ssid); + void signalStrengthChanged(quint8 signal); + void wpaFlagsChanged(NM80211ApSecurityFlags::Enum wpaFlags); + void rsnFlagsChanged(NM80211ApSecurityFlags::Enum rsnFlags); + void flagsChanged(NM80211ApFlags::Enum flags); + void modeChanged(NM80211Mode::Enum mode); + void ready(); + void disappeared(); + +private: + bool mActive = false; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, QByteArray, bSsid, &NMAccessPoint::ssidChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, quint8, bSignalStrength, &NMAccessPoint::signalStrengthChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApFlags::Enum, bFlags, &NMAccessPoint::flagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApSecurityFlags::Enum, bWpaFlags, &NMAccessPoint::wpaFlagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApSecurityFlags::Enum, bRsnFlags, &NMAccessPoint::rsnFlagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211Mode::Enum, bMode, &NMAccessPoint::modeChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMAccessPointAdapter, accessPointProperties); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pSsid, bSsid, accessPointProperties, "Ssid"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pSignalStrength, bSignalStrength, accessPointProperties, "Strength"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pFlags, bFlags, accessPointProperties, "Flags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pWpaFlags, bWpaFlags, accessPointProperties, "WpaFlags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pRsnFlags, bRsnFlags, accessPointProperties, "RsnFlags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pMode, bMode, accessPointProperties, "Mode"); + // clang-format on + + DBusNMAccessPointProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/backend.cpp b/src/network/nm/backend.cpp new file mode 100644 index 00000000..71a50740 --- /dev/null +++ b/src/network/nm/backend.cpp @@ -0,0 +1,228 @@ +#include "backend.hpp" + +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "../network.hpp" +#include "../wifi.hpp" +#include "device.hpp" +#include "nm/dbus_nm_backend.h" +#include "wireless.hpp" + +namespace qs::network { + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NetworkManager::NetworkManager(QObject* parent): NetworkBackend(parent) { + qDBusRegisterMetaType(); + + auto bus = QDBusConnection::systemBus(); + if (!bus.isConnected()) { + qCWarning(logNetworkManager + ) << "Could not connect to DBus. NetworkManager backend will not work."; + return; + } + + this->proxy = new DBusNetworkManagerProxy( + "org.freedesktop.NetworkManager", + "/org/freedesktop/NetworkManager", + bus, + this + ); + + if (!this->proxy->isValid()) { + qCDebug(logNetworkManager + ) << "NetworkManager is not currently running. This network backend will not work"; + } else { + this->init(); + } +} + +void NetworkManager::init() { + // clang-format off + QObject::connect(this->proxy, &DBusNetworkManagerProxy::DeviceAdded, this, &NetworkManager::onDevicePathAdded); + QObject::connect(this->proxy, &DBusNetworkManagerProxy::DeviceRemoved, this, &NetworkManager::onDevicePathRemoved); + // clang-format on + + this->dbusProperties.setInterface(this->proxy); + this->dbusProperties.updateAllViaGetAll(); + + this->registerDevices(); +} + +void NetworkManager::registerDevices() { + auto pending = this->proxy->GetAllDevices(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) << "Failed to get devices: " << reply.error().message(); + } else { + for (const QDBusObjectPath& devicePath: reply.value()) { + this->registerDevice(devicePath.path()); + } + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::registerDevice(const QString& path) { + if (this->mDeviceHash.contains(path)) { + qCDebug(logNetworkManager) << "Skipping duplicate registration of device" << path; + return; + } + + // Introspect to decide the device variant. (For now, only Wireless) + auto* introspection = new QDBusInterface( + "org.freedesktop.NetworkManager", + path, + "org.freedesktop.DBus.Introspectable", + QDBusConnection::systemBus(), + this + ); + + auto pending = introspection->asyncCall("Introspect"); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this, path, introspection](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) << "Failed to introspect device: " << reply.error().message(); + } else { + QXmlStreamReader xml(reply.value()); + + while (!xml.atEnd() && !xml.hasError()) { + xml.readNext(); + + if (xml.isStartElement() && xml.name() == "interface") { + QString name = xml.attributes().value("name").toString(); + if (name.startsWith("org.freedesktop.NetworkManager.Device.Wireless")) { + this->registerWifiDevice(path); + break; + } + } + } + } + delete call; + delete introspection; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +NetworkConnectionState::Enum NetworkManager::toNetworkDeviceState(NMDeviceState::Enum state) { + switch (state) { + case 0 ... 20: return NetworkConnectionState::Unknown; + case 30: return NetworkConnectionState::Disconnected; + case 40 ... 90: return NetworkConnectionState::Connecting; + case 100: return NetworkConnectionState::Connected; + case 110 ... 120: return NetworkConnectionState::Disconnecting; + } +} + +void NetworkManager::registerWifiDevice(const QString& path) { + auto* wireless = new NMWirelessDevice(path); + if (!wireless->isWirelessValid() || !wireless->isDeviceValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete wireless; + return; + } + + auto* device = new WifiDevice(this); + wireless->setParent(device); + this->mDeviceHash.insert(path, device); + + device->bindableName().setBinding([wireless]() { return wireless->interface(); }); + device->bindableAddress().setBinding([wireless]() { return wireless->hwAddress(); }); + device->bindableNmState().setBinding([wireless]() { return wireless->state(); }); + device->bindableState().setBinding([wireless]() { + return qs::network::NetworkManager::toNetworkDeviceState(wireless->state()); + }); + device->bindableScanning().setBinding([wireless]() { return wireless->scanning(); }); + // clang-format off + QObject::connect(wireless, &NMWirelessDevice::addAndActivateConnection, this, &NetworkManager::addAndActivateConnection); + QObject::connect(wireless, &NMWirelessDevice::activateConnection, this, &NetworkManager::activateConnection); + QObject::connect(wireless, &NMWirelessDevice::wifiNetworkAdded, device, &WifiDevice::networkAdded); + QObject::connect(wireless, &NMWirelessDevice::wifiNetworkRemoved, device, &WifiDevice::networkRemoved); + QObject::connect(device, &WifiDevice::requestScan, wireless, &NMWirelessDevice::scan); + QObject::connect(device, &WifiDevice::requestDisconnect, wireless, &NMWirelessDevice::disconnect); + // clang-format on + + emit this->wifiDeviceAdded(device); +} + +void NetworkManager::onDevicePathAdded(const QDBusObjectPath& path) { + this->registerDevice(path.path()); +} + +void NetworkManager::onDevicePathRemoved(const QDBusObjectPath& path) { + auto iter = this->mDeviceHash.find(path.path()); + if (iter == this->mDeviceHash.end()) { + qCWarning(logNetworkManager) << "Sent removal signal for" << path.path() + << "which is not registered."; + } else { + auto* device = iter.value(); + this->mDeviceHash.erase(iter); + if (auto* wifi = qobject_cast(device)) emit this->wifiDeviceRemoved(wifi); + delete device; + } +} + +void NetworkManager::activateConnection( + const QDBusObjectPath& connPath, + const QDBusObjectPath& devPath +) { + auto pending = this->proxy->ActivateConnection(connPath, devPath, QDBusObjectPath("/")); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) << "Failed to activate connection:" << reply.error().message(); + } + delete call; + }; + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::addAndActivateConnection( + const ConnectionSettingsMap& settings, + const QDBusObjectPath& devPath, + const QDBusObjectPath& specificObjectPath +) { + auto pending = this->proxy->AddAndActivateConnection(settings, devPath, specificObjectPath); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to add and activate connection:" << reply.error().message(); + } + delete call; + }; + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::setWifiEnabled(bool enabled) { + if (enabled == this->bWifiEnabled) return; + this->bWifiEnabled = enabled; + this->pWifiEnabled.write(); +} + +bool NetworkManager::isAvailable() const { return this->proxy && this->proxy->isValid(); }; + +} // namespace qs::network diff --git a/src/network/nm/backend.hpp b/src/network/nm/backend.hpp new file mode 100644 index 00000000..8582dbec --- /dev/null +++ b/src/network/nm/backend.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "../network.hpp" +#include "../wifi.hpp" +#include "device.hpp" +#include "nm/dbus_nm_backend.h" + +namespace qs::network { + +class NetworkManager: public NetworkBackend { + Q_OBJECT; + +signals: + void wifiEnabledChanged(bool enabled); + void wifiHardwareEnabledChanged(bool enabled); + void wifiDeviceAdded(WifiDevice* device); + void wifiDeviceRemoved(WifiDevice* device); + +public: + explicit NetworkManager(QObject* parent = nullptr); + [[nodiscard]] bool isAvailable() const override; + [[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; }; + [[nodiscard]] bool wifiHardwareEnabled() const { return this->bWifiHardwareEnabled; }; + +public slots: + void setWifiEnabled(bool enabled); + +private slots: + void onDevicePathAdded(const QDBusObjectPath& path); + void onDevicePathRemoved(const QDBusObjectPath& path); + void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath); + void addAndActivateConnection( + const ConnectionSettingsMap& settings, + const QDBusObjectPath& devPath, + const QDBusObjectPath& specificObjectPath + ); + +private: + void init(); + void registerDevices(); + void registerDevice(const QString& path); + void registerWifiDevice(const QString& path); + static NetworkConnectionState::Enum toNetworkDeviceState(NMDeviceState::Enum state); + + QHash mDeviceHash; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiEnabled, &NetworkManager::wifiEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiHardwareEnabled, &NetworkManager::wifiHardwareEnabledChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NetworkManager, dbusProperties); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiEnabled, bWifiEnabled, dbusProperties, "WirelessEnabled"); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiHardwareEnabled, bWifiHardwareEnabled, dbusProperties, "WirelessHardwareEnabled"); + // clang-format on + DBusNetworkManagerProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/connection.cpp b/src/network/nm/connection.cpp new file mode 100644 index 00000000..4330fb67 --- /dev/null +++ b/src/network/nm/connection.cpp @@ -0,0 +1,118 @@ +#include "connection.hpp" + +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMConnectionSettings::NMConnectionSettings(const QString& path, QObject* parent): QObject(parent) { + qDBusRegisterMetaType(); + + this->proxy = new DBusNMConnectionSettingsProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; + return; + } + + QObject::connect( + this->proxy, + &DBusNMConnectionSettingsProxy::Updated, + this, + &NMConnectionSettings::updateSettings + ); + + this->connectionSettingsProperties.setInterface(this->proxy); + this->connectionSettingsProperties.updateAllViaGetAll(); + + this->updateSettings(); +} + +void NMConnectionSettings::updateSettings() { + auto pending = this->proxy->GetSettings(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to get" << this->path() << "settings:" << reply.error().message(); + } else { + this->bSettings = reply.value(); + } + + emit this->ready(); + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +bool NMConnectionSettings::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMConnectionSettings::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMConnectionSettings::path() const { return this->proxy ? this->proxy->path() : QString(); } + +NMActiveConnection::NMActiveConnection(const QString& path, QObject* parent): QObject(parent) { + this->proxy = new DBusNMActiveConnectionProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; + return; + } + + // clang-format off + QObject::connect(&this->activeConnectionProperties, &DBusPropertyGroup::getAllFinished, this, &NMActiveConnection::ready, Qt::SingleShotConnection); + QObject::connect(this->proxy, &DBusNMActiveConnectionProxy::StateChanged, this, &NMActiveConnection::onStateChanged); + // clang-format on + + this->activeConnectionProperties.setInterface(this->proxy); + this->activeConnectionProperties.updateAllViaGetAll(); +} + +void NMActiveConnection::onStateChanged(quint32 /*state*/, quint32 reason) { + auto enumReason = static_cast(reason); + if (this->mStateReason == enumReason) return; + this->mStateReason = enumReason; + emit this->stateReasonChanged(enumReason); +} + +bool NMActiveConnection::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMActiveConnection::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMActiveConnection::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/connection.hpp b/src/network/nm/connection.hpp new file mode 100644 index 00000000..07b034d2 --- /dev/null +++ b/src/network/nm/connection.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "dbus_types.hpp" +#include "enums.hpp" +#include "nm/dbus_nm_active_connection.h" +#include "nm/dbus_nm_connection_settings.h" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMConnectionState::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +// Proxy of a /org/freedesktop/NetworkManager/Settings/Connection/* object. +class NMConnectionSettings: public QObject { + Q_OBJECT; + +public: + explicit NMConnectionSettings(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] ConnectionSettingsMap settings() const { return this->bSettings; }; + +signals: + void settingsChanged(ConnectionSettingsMap settings); + void ssidChanged(QString ssid); + void ready(); + void disappeared(); + +private: + void updateSettings(); + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettings, ConnectionSettingsMap, bSettings, &NMConnectionSettings::settingsChanged); + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMConnectionSettings, connectionSettingsProperties); + // clang-format on + + DBusNMConnectionSettingsProxy* proxy = nullptr; +}; + +// Proxy of a /org/freedesktop/NetworkManager/ActiveConnection/* object. +class NMActiveConnection: public QObject { + Q_OBJECT; + +public: + explicit NMActiveConnection(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QDBusObjectPath connection() const { return this->bConnection; }; + [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; }; + [[nodiscard]] NMConnectionStateReason::Enum stateReason() const { return this->mStateReason; }; + [[nodiscard]] QString uuid() const { return this->bUuid; }; + +signals: + void stateChanged(NMConnectionState::Enum state); + void stateReasonChanged(NMConnectionStateReason::Enum reason); + void connectionChanged(QDBusObjectPath path); + void uuidChanged(const QString& uuid); + void ready(); + void disappeared(); + +private slots: + void onStateChanged(quint32 state, quint32 reason); + +private: + NMConnectionStateReason::Enum mStateReason = NMConnectionStateReason::Unknown; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QDBusObjectPath, bConnection, &NMActiveConnection::connectionChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QString, bUuid, &NMActiveConnection::uuidChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, NMConnectionState::Enum, bState, &NMActiveConnection::stateChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMActiveConnection, activeConnectionProperties); + QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pConnection, bConnection, activeConnectionProperties, "Connection"); + QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pUuid, bUuid, activeConnectionProperties, "Uuid"); + QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pState, bState, activeConnectionProperties, "State"); + // clang-format on + DBusNMActiveConnectionProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/dbus_types.hpp b/src/network/nm/dbus_types.hpp new file mode 100644 index 00000000..dadbcf38 --- /dev/null +++ b/src/network/nm/dbus_types.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include +#include + +using ConnectionSettingsMap = QMap; +Q_DECLARE_METATYPE(ConnectionSettingsMap); diff --git a/src/network/nm/device.cpp b/src/network/nm/device.cpp new file mode 100644 index 00000000..51dcd0eb --- /dev/null +++ b/src/network/nm/device.cpp @@ -0,0 +1,130 @@ +#include "device.hpp" + +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMDevice::NMDevice(const QString& path, QObject* parent): QObject(parent) { + this->deviceProxy = new DBusNMDeviceProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->deviceProxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for device at" << path; + return; + } + + // clang-format off + QObject::connect(this, &NMDevice::availableConnectionPathsChanged, this, &NMDevice::onAvailableConnectionPathsChanged); + QObject::connect(this, &NMDevice::activeConnectionPathChanged, this, &NMDevice::onActiveConnectionPathChanged); + QObject::connect(&this->deviceProperties, &DBusPropertyGroup::getAllFinished, this, &NMDevice::deviceReady, Qt::SingleShotConnection); + // clang-format on + + this->deviceProperties.setInterface(this->deviceProxy); + this->deviceProperties.updateAllViaGetAll(); +} + +void NMDevice::onActiveConnectionPathChanged(const QDBusObjectPath& path) { + QString stringPath = path.path(); + + // Remove old active connection + if (this->mActiveConnection) { + QObject::disconnect(this->mActiveConnection, nullptr, this, nullptr); + emit this->mActiveConnection->disappeared(); + delete this->mActiveConnection; + this->mActiveConnection = nullptr; + } + + // Create new active connection + if (stringPath != "/") { + auto* active = new NMActiveConnection(stringPath, this); + if (!active->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << stringPath; + delete active; + } else { + this->mActiveConnection = active; + QObject::connect( + active, + &NMActiveConnection::ready, + this, + [this, active]() { emit this->activeConnectionLoaded(active); }, + Qt::SingleShotConnection + ); + } + } +} + +void NMDevice::onAvailableConnectionPathsChanged(const QList& paths) { + QSet newConnectionPaths; + for (const QDBusObjectPath& path: paths) { + newConnectionPaths.insert(path.path()); + } + + QSet addedConnections = newConnectionPaths - this->mConnectionPaths; + QSet removedConnections = this->mConnectionPaths - newConnectionPaths; + for (const QString& path: addedConnections) { + registerConnection(path); + } + for (const QString& path: removedConnections) { + auto* connection = this->mConnectionMap.take(path); + if (!connection) { + qCDebug(logNetworkManager) << "NetworkManager backend sent removal signal for" << path + << "which is not registered."; + } else { + emit connection->disappeared(); + delete connection; + } + this->mConnectionPaths.remove(path); + }; +} + +void NMDevice::registerConnection(const QString& path) { + auto* connection = new NMConnectionSettings(path, this); + if (!connection->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete connection; + } else { + this->mConnectionMap.insert(path, connection); + this->mConnectionPaths.insert(path); + QObject::connect( + connection, + &NMConnectionSettings::ready, + this, + [this, connection]() { emit this->connectionLoaded(connection); }, + Qt::SingleShotConnection + ); + } +} + +void NMDevice::disconnect() { this->deviceProxy->Disconnect(); } +bool NMDevice::isDeviceValid() const { return this->deviceProxy && this->deviceProxy->isValid(); } +QString NMDevice::address() const { + return this->deviceProxy ? this->deviceProxy->service() : QString(); +} +QString NMDevice::path() const { return this->deviceProxy ? this->deviceProxy->path() : QString(); } + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/device.hpp b/src/network/nm/device.hpp new file mode 100644 index 00000000..f431df66 --- /dev/null +++ b/src/network/nm/device.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "connection.hpp" +#include "enums.hpp" +#include "nm/dbus_nm_device.h" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMDeviceState::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +// Proxy of a /org/freedesktop/NetworkManager/Device/* object. +// Only the members from the org.freedesktop.NetworkManager.Device interface. +// Owns the lifetime of NMActiveConnection(s) and NMConnectionSetting(s). +class NMDevice: public QObject { + Q_OBJECT; + +public: + explicit NMDevice(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isDeviceValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QString interface() const { return this->bInterface; }; + [[nodiscard]] QString hwAddress() const { return this->bHwAddress; }; + [[nodiscard]] NMDeviceState::Enum state() const { return this->bState; }; + [[nodiscard]] NMActiveConnection* activeConnection() const { return this->mActiveConnection; }; + +public slots: + void disconnect(); + +signals: + void interfaceChanged(const QString& interface); + void hwAddressChanged(const QString& hwAddress); + void stateChanged(NMDeviceState::Enum state); + void connectionLoaded(NMConnectionSettings* connection); + void connectionRemoved(NMConnectionSettings* connection); + void availableConnectionPathsChanged(QList paths); + void activeConnectionPathChanged(const QDBusObjectPath& connection); + void activeConnectionLoaded(NMActiveConnection* active); + void deviceReady(); + +private slots: + void onAvailableConnectionPathsChanged(const QList& paths); + void onActiveConnectionPathChanged(const QDBusObjectPath& path); + +private: + void registerConnection(const QString& path); + + QSet mConnectionPaths; + QHash mConnectionMap; + NMActiveConnection* mActiveConnection = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QString, bInterface, &NMDevice::interfaceChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QString, bHwAddress, &NMDevice::hwAddressChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, NMDeviceState::Enum, bState, &NMDevice::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QList, bAvailableConnections, &NMDevice::availableConnectionPathsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QDBusObjectPath, bActiveConnection, &NMDevice::activeConnectionPathChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMDeviceAdapter, deviceProperties); + QS_DBUS_PROPERTY_BINDING(NMDevice, pName, bInterface, deviceProperties, "Interface"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pAddress, bHwAddress, deviceProperties, "HwAddress"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pState, bState, deviceProperties, "State"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pAvailableConnections, bAvailableConnections, deviceProperties, "AvailableConnections"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pActiveConnection, bActiveConnection, deviceProperties, "ActiveConnection"); + // clang-format on + + DBusNMDeviceProxy* deviceProxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/enums.hpp b/src/network/nm/enums.hpp new file mode 100644 index 00000000..c9aa2cf9 --- /dev/null +++ b/src/network/nm/enums.hpp @@ -0,0 +1,255 @@ +#pragma once + +#include +#include +#include +#include + +namespace qs::network { + +// 802.11 specific device encryption and authentication capabilities. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceWifiCapabilities. +class NMWirelessCapabilities: public QObject { + Q_OBJECT; + +public: + enum Enum : quint16 { + None = 0, + CipherWep40 = 1, + CipherWep104 = 2, + CipherTkip = 4, + CipherCcmp = 8, + Wpa = 16, + Rsn = 32, + Ap = 64, + Adhoc = 128, + FreqValid = 256, + Freq2Ghz = 512, + Freq5Ghz = 1024, + Freq6Ghz = 2048, + Mesh = 4096, + IbssRsn = 8192, + }; + Q_ENUM(Enum); +}; + +class NMWirelessSecurityType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Wpa3SuiteB192 = 0, + Sae = 1, + Wpa2Eap = 2, + Wpa2Psk = 3, + WpaEap = 4, + WpaPsk = 5, + StaticWep = 6, + DynamicWep = 7, + Leap = 8, + Owe = 9, + None = 10, + Unknown = 11, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NMWirelessSecurityType::Enum type) { + switch (type) { + case Wpa3SuiteB192: return QStringLiteral("WPA3 Suite B 192-bit"); + case Sae: return QStringLiteral("WPA3"); + case Wpa2Eap: return QStringLiteral("WPA2 Enterprise"); + case Wpa2Psk: return QStringLiteral("WPA2"); + case WpaEap: return QStringLiteral("WPA Enterprise"); + case WpaPsk: return QStringLiteral("WPA"); + case StaticWep: return QStringLiteral("WEP"); + case DynamicWep: return QStringLiteral("Dynamic WEP"); + case Leap: return QStringLiteral("LEAP"); + case Owe: return QStringLiteral("OWE"); + case None: return QStringLiteral("None"); + default: return QStringLiteral("Unknown"); + } + } +}; + +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState. +class NMDeviceState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Unmanaged = 10, + Unavailable = 20, + Disconnected = 30, + Prepare = 40, + Config = 50, + NeedAuth = 60, + IPConfig = 70, + IPCheck = 80, + Secondaries = 90, + Activated = 100, + Deactivating = 110, + Failed = 120, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NMDeviceState::Enum state) { + switch (state) { + case Unknown: return QStringLiteral("Unknown"); + case Unmanaged: return QStringLiteral("Not managed by NetworkManager"); + case Unavailable: return QStringLiteral("Unavailable"); + case Disconnected: return QStringLiteral("Disconnected"); + case Prepare: return QStringLiteral("Preparing to connect"); + case Config: return QStringLiteral("Connecting to a network"); + case NeedAuth: return QStringLiteral("Waiting for authentication"); + case IPConfig: return QStringLiteral("Requesting IPv4 and/or IPv6 addresses from the network"); + case IPCheck: + return QStringLiteral( + "Checking whether further action is required for the requested connection" + ); + case Secondaries: + return QStringLiteral("Waiting for a required secondary connection to activate"); + case Activated: return QStringLiteral("Connected"); + case Deactivating: return QStringLiteral("Disconnecting"); + case Failed: return QStringLiteral("Failed to connect"); + }; + }; +}; + +// Indicates the 802.11 mode an access point is currently in. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211Mode. +class NM80211Mode: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + Adhoc = 1, + Infra = 2, + Ap = 3, + Mesh = 3, + }; + Q_ENUM(Enum); +}; + +// 802.11 access point flags. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211ApSecurityFlags. +class NM80211ApFlags: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + None = 0, + Privacy = 1, + Wps = 2, + WpsPbc = 4, + WpsPin = 8, + }; + Q_ENUM(Enum); +}; + +// 802.11 access point security and authentication flags. +// These flags describe the current system requirements of an access point as determined from the access point's beacon. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211ApSecurityFlags. +class NM80211ApSecurityFlags: public QObject { + Q_OBJECT; + +public: + enum Enum : quint16 { + None = 0, + PairWep40 = 1, + PairWep104 = 2, + PairTkip = 4, + PairCcmp = 8, + GroupWep40 = 16, + GroupWep104 = 32, + GroupTkip = 64, + GroupCcmp = 128, + KeyMgmtPsk = 256, + KeyMgmt8021x = 512, + KeyMgmtSae = 1024, + KeyMgmtOwe = 2048, + KeyMgmtOweTm = 4096, + KeyMgmtEapSuiteB192 = 8192, + }; + Q_ENUM(Enum); +}; + +// Indicates the state of a connection to a specific network while it is starting, connected, or disconnected from that network. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionState. +class NMConnectionState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Activating = 1, + Activated = 2, + Deactivating = 3, + Deactivated = 4 + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NMConnectionState::Enum state) { + switch (state) { + case Unknown: return "Unknown"; + case Activating: return "Activating"; + case Activated: return "Activated"; + case Deactivating: return "Deactivating"; + case Deactivated: return "Deactivated"; + } + } +}; + +// Active connection state reasons. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionStateReason. +class NMConnectionStateReason: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + None = 1, + UserDisconnected = 2, + DeviceDisconnected = 3, + ServiceStopped = 4, + IpConfigInvalid = 5, + ConnectTimeout = 6, + ServiceStartTimeout = 7, + ServiceStartFailed = 8, + NoSecrets = 9, + LoginFailed = 10, + ConnectionRemoved = 11, + DependencyFailed = 12, + DeviceRealizeFailed = 13, + DeviceRemoved = 14 + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NMConnectionStateReason::Enum reason) { + switch (reason) { + case Unknown: return "Unknown"; + case None: return "No reason"; + case UserDisconnected: return "User disconnection"; + case DeviceDisconnected: return "The device the connection was using was disconnected."; + case ServiceStopped: return "The service providing the VPN connection was stopped."; + case IpConfigInvalid: return "The IP config of the active connection was invalid."; + case ConnectTimeout: return "The connection attempt to the VPN service timed out."; + case ServiceStartTimeout: + return "A timeout occurred while starting the service providing the VPN connection."; + case ServiceStartFailed: return "Starting the service providing the VPN connection failed."; + case NoSecrets: return "Necessary secrets for the connection were not provided."; + case LoginFailed: return "Authentication to the server failed."; + case ConnectionRemoved: return "Necessary secrets for the connection were not provided."; + case DependencyFailed: return " Master connection of this connection failed to activate."; + case DeviceRealizeFailed: return "Could not create the software device link."; + case DeviceRemoved: return "The device this connection depended on disappeared."; + }; + }; +}; + +} // namespace qs::network diff --git a/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml b/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml new file mode 100644 index 00000000..c5e7737d --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml b/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml new file mode 100644 index 00000000..fa0e778c --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml b/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml new file mode 100644 index 00000000..984f43dc --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Device.xml b/src/network/nm/org.freedesktop.NetworkManager.Device.xml new file mode 100644 index 00000000..322635f3 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Device.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml new file mode 100644 index 00000000..22a2b0d3 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.xml b/src/network/nm/org.freedesktop.NetworkManager.xml new file mode 100644 index 00000000..6165af1d --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/network/nm/utils.cpp b/src/network/nm/utils.cpp new file mode 100644 index 00000000..0d475601 --- /dev/null +++ b/src/network/nm/utils.cpp @@ -0,0 +1,229 @@ +#include "utils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dbus_types.hpp" +#include "enums.hpp" + +namespace qs::network { + +NMWirelessSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings) { + const QVariantMap& security = settings.value("802-11-wireless-security"); + if (security.isEmpty()) { + return NMWirelessSecurityType::Unknown; + }; + + QString keyMgmt = security["key-mgmt"].toString(); + QString authAlg = security["auth-alg"].toString(); + QList proto = security["proto"].toList(); + + if (keyMgmt == "none") { + return NMWirelessSecurityType::StaticWep; + } else if (keyMgmt == "ieee8021x") { + if (authAlg == "leap") { + return NMWirelessSecurityType::Leap; + } else { + return NMWirelessSecurityType::DynamicWep; + } + } else if (keyMgmt == "wpa-psk") { + if (proto.contains("wpa") && proto.contains("rsn")) return NMWirelessSecurityType::WpaPsk; + return NMWirelessSecurityType::Wpa2Psk; + } else if (keyMgmt == "wpa-eap") { + if (proto.contains("wpa") && proto.contains("rsn")) return NMWirelessSecurityType::WpaEap; + return NMWirelessSecurityType::Wpa2Eap; + } else if (keyMgmt == "sae") { + return NMWirelessSecurityType::Sae; + } else if (keyMgmt == "wpa-eap-suite-b-192") { + return NMWirelessSecurityType::Wpa3SuiteB192; + } + return NMWirelessSecurityType::None; +} + +bool deviceSupportsApCiphers( + NMWirelessCapabilities::Enum caps, + NM80211ApSecurityFlags::Enum apFlags, + NMWirelessSecurityType::Enum type +) { + bool havePair = false; + bool haveGroup = false; + // Device needs to support at least one pairwise and one group cipher + + if (type == NMWirelessSecurityType::StaticWep) { + // Static WEP only uses group ciphers + havePair = true; + } else { + if (caps & NMWirelessCapabilities::CipherWep40 && apFlags & NM80211ApSecurityFlags::PairWep40) { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherWep104 && apFlags & NM80211ApSecurityFlags::PairWep104) + { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherTkip && apFlags & NM80211ApSecurityFlags::PairTkip) { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherCcmp && apFlags & NM80211ApSecurityFlags::PairCcmp) { + havePair = true; + } + } + + if (caps & NMWirelessCapabilities::CipherWep40 && apFlags & NM80211ApSecurityFlags::GroupWep40) { + haveGroup = true; + } + if (caps & NMWirelessCapabilities::CipherWep104 && apFlags & NM80211ApSecurityFlags::GroupWep104) + { + haveGroup = true; + } + if (type == NMWirelessSecurityType::StaticWep) { + if (caps & NMWirelessCapabilities::CipherTkip && apFlags & NM80211ApSecurityFlags::GroupTkip) { + haveGroup = true; + } + if (caps & NMWirelessCapabilities::CipherCcmp && apFlags & NM80211ApSecurityFlags::GroupCcmp) { + haveGroup = true; + } + } + + return (havePair && haveGroup); +} + +// In sync with NetworkManager/libnm-core/nm-utils.c:nm_utils_security_valid() +// Given a set of device capabilities, and a desired security type to check +// against, determines whether the combination of device, desired security type, +// and AP capabilities intersect. +bool securityIsValid( + NMWirelessSecurityType::Enum type, + NMWirelessCapabilities::Enum caps, + bool adhoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +) { + switch (type) { + case NMWirelessSecurityType::None: + if (apFlags & NM80211ApFlags::Privacy) return false; + if (apWpa || apRsn) return false; + break; + case NMWirelessSecurityType::Leap: + if (adhoc) return false; + case NMWirelessSecurityType::StaticWep: + if (!(apFlags & NM80211ApFlags::Privacy)) return false; + if (apWpa || apRsn) { + if (!deviceSupportsApCiphers(caps, apWpa, NMWirelessSecurityType::StaticWep)) { + if (!deviceSupportsApCiphers(caps, apRsn, NMWirelessSecurityType::StaticWep)) return false; + } + } + break; + case NMWirelessSecurityType::DynamicWep: + if (adhoc) return false; + if (apRsn || !(apFlags & NM80211ApFlags::Privacy)) return false; + if (apWpa) { + if (!(apWpa & NM80211ApSecurityFlags::KeyMgmt8021x)) return false; + if (!deviceSupportsApCiphers(caps, apWpa, NMWirelessSecurityType::DynamicWep)) return false; + } + break; + case NMWirelessSecurityType::WpaPsk: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Wpa)) return false; + if (apWpa & NM80211ApSecurityFlags::KeyMgmtPsk) { + if (apWpa & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) { + return true; + } + if (apWpa & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } + return false; + case NMWirelessSecurityType::Wpa2Psk: + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (adhoc) { + if (!(caps & NMWirelessCapabilities::IbssRsn)) return false; + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } else { + if (apRsn & NM80211ApSecurityFlags::KeyMgmtPsk) { + if (apRsn & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) { + return true; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } + } + return false; + case NMWirelessSecurityType::WpaEap: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Wpa)) return false; + if (!(apWpa & NM80211ApSecurityFlags::KeyMgmt8021x)) return false; + if (!deviceSupportsApCiphers(caps, apWpa, NMWirelessSecurityType::WpaEap)) return false; + break; + case NMWirelessSecurityType::Wpa2Eap: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (!(apRsn & NM80211ApSecurityFlags::KeyMgmt8021x)) return false; + if (!deviceSupportsApCiphers(caps, apRsn, NMWirelessSecurityType::Wpa2Eap)) return false; + break; + case NMWirelessSecurityType::Sae: + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (adhoc) { + if (!(caps & NMWirelessCapabilities::IbssRsn)) return false; + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } else { + if (apRsn & NM80211ApSecurityFlags::KeyMgmtSae) { + if (apRsn & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) { + return true; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } + } + return false; + case NMWirelessSecurityType::Owe: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (!(apRsn & NM80211ApSecurityFlags::KeyMgmtOwe) + && !(apRsn & NM80211ApSecurityFlags::KeyMgmtOweTm)) + { + return false; + } + break; + case NMWirelessSecurityType::Wpa3SuiteB192: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (!(apRsn & NM80211ApSecurityFlags::KeyMgmtEapSuiteB192)) return false; + break; + default: return false; + } + return true; +} + +NMWirelessSecurityType::Enum findBestWirelessSecurity( + NMWirelessCapabilities::Enum caps, + bool adHoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +) { + // Loop through security types from most to least secure since the enum + // values are sequential and in priority order (0-10, excluding Unknown=11) + for (int i = NMWirelessSecurityType::Wpa3SuiteB192; i <= NMWirelessSecurityType::None; ++i) { + auto type = static_cast(i); + if (securityIsValid(type, caps, adHoc, apFlags, apWpa, apRsn)) { + return type; + } + } + return NMWirelessSecurityType::Unknown; +} + +} // namespace qs::network diff --git a/src/network/nm/utils.hpp b/src/network/nm/utils.hpp new file mode 100644 index 00000000..568da53a --- /dev/null +++ b/src/network/nm/utils.hpp @@ -0,0 +1,44 @@ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dbus_types.hpp" +#include "enums.hpp" + +namespace qs::network { + +NMWirelessSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings); + +bool deviceSupportsApCiphers( + NMWirelessCapabilities::Enum caps, + NM80211ApSecurityFlags::Enum apFlags, + NMWirelessSecurityType::Enum type +); + +bool securityIsValid( + NMWirelessSecurityType::Enum type, + NMWirelessCapabilities::Enum caps, + bool adhoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +); + +NMWirelessSecurityType::Enum findBestWirelessSecurity( + NMWirelessCapabilities::Enum caps, + bool adHoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +); + +} // namespace qs::network diff --git a/src/network/nm/wireless.cpp b/src/network/nm/wireless.cpp new file mode 100644 index 00000000..3da778f1 --- /dev/null +++ b/src/network/nm/wireless.cpp @@ -0,0 +1,373 @@ +#include "wireless.hpp" + +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "dbus_types.hpp" +#include "nm/enums.hpp" +#include "nm/utils.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMWirelessNetwork::NMWirelessNetwork(QString ssid, QObject* parent) + : QObject(parent) + , mSsid(std::move(ssid)) + , bKnown(false) + , bReason(NMConnectionStateReason::None) + , bState(NMConnectionState::Deactivated) + , bSecurity(NMWirelessSecurityType::None) {} + +void NMWirelessNetwork::updateReferenceConnection() { + // If the network has no connections, the reference is nullptr. + if (this->mConnections.isEmpty()) { + this->mReferenceConn = nullptr; + this->updateReferenceAp(); // Set security back to reference AP. + return; + }; + + // If the network has an active connection, use it as the reference. + if (this->mActiveConnection) { + auto* ref = mConnections.value(this->mActiveConnection->connection().path()); + if (ref) { + this->mReferenceConn = ref; + return; + } + } + + // Otherwise, choose the connection with the strongest security settings. + auto selectedSecurity = NMWirelessSecurityType::Unknown; + NMConnectionSettings* selectedConn = nullptr; + for (auto* conn: this->mConnections) { + auto security = securityFromConnectionSettings(conn->settings()); + if (selectedSecurity >= security) { + selectedSecurity = security; + selectedConn = conn; + } + } + if (selectedConn && this->mReferenceConn != selectedConn) { + this->mReferenceConn = selectedConn; + this->bSecurity = selectedSecurity; + } +} + +void NMWirelessNetwork::updateReferenceAp() { + quint8 selectedStrength = 0; + NMAccessPoint* selectedAp = nullptr; + + for (auto* ap: this->mAccessPoints.values()) { + // Always prefer the active AP if found. + if (ap->path() == this->bActiveApPath) { + selectedStrength = ap->signalStrength(); + selectedAp = ap; + break; + } + + // Otherwise, track the strongest signal. + if (selectedStrength <= ap->signalStrength()) { + selectedStrength = ap->signalStrength(); + selectedAp = ap; + } + } + + if (selectedStrength != this->bSignalStrength) { + this->bSignalStrength = selectedStrength; + } + + if (selectedAp && this->mReferenceAp != selectedAp) { + // Update reference AP. + this->mReferenceAp = selectedAp; + // Reference AP is used for security when there's no connection settings. + if (this->mReferenceConn) return; + auto security = findBestWirelessSecurity( + this->bCaps, + this->mReferenceAp->mode() == NM80211Mode::Adhoc, + this->mReferenceAp->flags(), + this->mReferenceAp->wpaFlags(), + this->mReferenceAp->rsnFlags() + ); + this->bSecurity = security; + } +} + +void NMWirelessNetwork::addAccessPoint(NMAccessPoint* ap) { + if (this->mAccessPoints.contains(ap->path())) return; + this->mAccessPoints.insert(ap->path(), ap); + // clang-format off + QObject::connect(ap, &NMAccessPoint::signalStrengthChanged, this, &NMWirelessNetwork::updateReferenceAp); + QObject::connect(ap, &NMAccessPoint::disappeared, this, &NMWirelessNetwork::removeAccessPoint); + // clang-format on + this->updateReferenceAp(); +}; + +void NMWirelessNetwork::removeAccessPoint() { + auto* ap = qobject_cast(sender()); + if (this->mAccessPoints.take(ap->path())) { + if (this->mAccessPoints.isEmpty()) { + emit this->disappeared(); + } else { + QObject::disconnect(ap, nullptr, this, nullptr); + this->updateReferenceAp(); + } + } +}; + +void NMWirelessNetwork::addConnectionSettings(NMConnectionSettings* conn) { + if (this->mConnections.contains(conn->path())) return; + this->mConnections.insert(conn->path(), conn); + QObject::connect( + conn, + &NMConnectionSettings::disappeared, + this, + &NMWirelessNetwork::removeConnectionSettings + ); + this->updateReferenceConnection(); + this->bKnown = true; +}; + +void NMWirelessNetwork::removeConnectionSettings() { + auto* conn = qobject_cast(sender()); + if (this->mConnections.take(conn->path())) { + QObject::disconnect(conn, nullptr, this, nullptr); + this->updateReferenceConnection(); + if (mConnections.isEmpty()) { + this->bKnown = false; + } + } +}; + +void NMWirelessNetwork::addActiveConnection(NMActiveConnection* active) { + if (this->mActiveConnection) return; + this->mActiveConnection = active; + this->bState.setBinding([active]() { return active->state(); }); + this->bReason.setBinding([active]() { return active->stateReason(); }); + QObject::connect( + active, + &NMActiveConnection::disappeared, + this, + &NMWirelessNetwork::removeActiveConnection + ); +}; + +void NMWirelessNetwork::removeActiveConnection() { + auto* active = qobject_cast(sender()); + if (this->mActiveConnection && this->mActiveConnection == active) { + QObject::disconnect(active, nullptr, this, nullptr); + this->bState = NMConnectionState::Deactivated; + this->bReason = NMConnectionStateReason::None; + this->mActiveConnection = nullptr; + } +}; + +NMWirelessDevice::NMWirelessDevice(const QString& path, QObject* parent): NMDevice(path, parent) { + this->wirelessProxy = new DBusNMWirelessProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->wirelessProxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for wireless device at" << path; + return; + } + + QObject::connect( + &this->wirelessProperties, + &DBusPropertyGroup::getAllFinished, + this, + &NMWirelessDevice::initWireless, + Qt::SingleShotConnection + ); + + this->wirelessProperties.setInterface(this->wirelessProxy); + this->wirelessProperties.updateAllViaGetAll(); +} + +void NMWirelessDevice::initWireless() { + // clang-format off + QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointAdded, this, &NMWirelessDevice::onAccessPointPathAdded); + QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointRemoved, this, &NMWirelessDevice::onAccessPointPathRemoved); + QObject::connect(this, &NMWirelessDevice::accessPointLoaded, this, &NMWirelessDevice::onAccessPointLoaded); + QObject::connect(this, &NMWirelessDevice::connectionLoaded, this, &NMWirelessDevice::onConnectionLoaded); + QObject::connect(this, &NMWirelessDevice::activeConnectionLoaded, this, &NMWirelessDevice::onActiveConnectionLoaded); + QObject::connect(this, &NMWirelessDevice::lastScanChanged, this, [this]() { this->bScanning = false; }); + // clang-format on + this->registerAccessPoints(); +} + +void NMWirelessDevice::onAccessPointPathAdded(const QDBusObjectPath& path) { + this->registerAccessPoint(path.path()); +} + +void NMWirelessDevice::onAccessPointPathRemoved(const QDBusObjectPath& path) { + auto* ap = mAccessPoints.take(path.path()); + if (!ap) { + qCDebug(logNetworkManager) << "Sent removal signal for" << path.path() + << "which is not registered."; + return; + } + emit ap->disappeared(); + delete ap; +} + +void NMWirelessDevice::registerAccessPoints() { + auto pending = this->wirelessProxy->GetAllAccessPoints(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to get all access points: " << reply.error().message(); + } else { + for (const QDBusObjectPath& devicePath: reply.value()) { + this->registerAccessPoint(devicePath.path()); + } + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NMWirelessDevice::registerAccessPoint(const QString& path) { + if (this->mAccessPoints.contains(path)) { + qCDebug(logNetworkManager) << "Skipping duplicate registration of access point" << path; + return; + } + + auto* ap = new NMAccessPoint(path, this); + + if (!ap->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete ap; + return; + } + + this->mAccessPoints.insert(path, ap); + QObject::connect( + ap, + &NMAccessPoint::ready, + this, + [this, ap]() { emit this->accessPointLoaded(ap); }, + Qt::SingleShotConnection + ); +} + +NMWirelessNetwork* NMWirelessDevice::registerNetwork(const QString& ssid) { + auto* backend = new NMWirelessNetwork(ssid, this); + backend->bindableActiveApPath().setBinding([this]() { return this->activeApPath().path(); }); + backend->bindableCapabilities().setBinding([this]() { return this->capabilities(); }); + + auto* frontend = new WifiNetwork(ssid, this); + frontend->bindableSignalStrength().setBinding([backend]() { return backend->signalStrength(); }); + frontend->bindableConnected().setBinding([backend]() { + return backend->state() != NMConnectionState::Deactivated; + }); + frontend->bindableKnown().setBinding([backend]() { return backend->known(); }); + frontend->bindableNmReason().setBinding([backend]() { return backend->reason(); }); + frontend->bindableNmSecurity().setBinding([backend]() { return backend->security(); }); + QObject::connect(backend, &NMWirelessNetwork::disappeared, this, [this, frontend, backend]() { + QObject::disconnect(backend, nullptr, nullptr, nullptr); + emit this->wifiNetworkRemoved(frontend); + this->mBackendNetworks.remove(backend->ssid()); + delete backend; + delete frontend; + }); + QObject::connect(frontend, &WifiNetwork::requestConnect, this, [this, backend]() { + if (backend->referenceConnection()) { + emit this->activateConnection( + QDBusObjectPath(backend->referenceConnection()->path()), + QDBusObjectPath(this->path()) + ); + return; + } + if (backend->referenceAp()) { + emit this->addAndActivateConnection( + ConnectionSettingsMap(), + QDBusObjectPath(this->path()), + QDBusObjectPath(backend->referenceAp()->path()) + ); + } + }); + + this->mBackendNetworks.insert(ssid, backend); + emit this->wifiNetworkAdded(frontend); + return backend; +} + +void NMWirelessDevice::onAccessPointLoaded(NMAccessPoint* ap) { + QString ssid = ap->ssid(); + if (!ssid.isEmpty()) { + auto* net = this->mBackendNetworks.value(ssid); + if (!net) net = registerNetwork(ssid); + net->addAccessPoint(ap); + } +} + +void NMWirelessDevice::onConnectionLoaded(NMConnectionSettings* conn) { + const ConnectionSettingsMap& settings = conn->settings(); + // Skip connections that have empty settings, + // or who are this devices own hotspots. + if (settings["connection"]["id"].toString().isEmpty() + || settings["connection"]["uuid"].toString().isEmpty() + || !settings.contains("802-11-wireless") + || settings["802-11-wireless"]["mode"].toString() == "ap" + || settings["802-11-wireless"]["ssid"].toString().isEmpty()) + { + return; + } + + const QString ssid = settings["802-11-wireless"]["ssid"].toString(); + auto* net = mBackendNetworks.value(ssid); + if (!net) net = registerNetwork(ssid); + + net->addConnectionSettings(conn); + auto* active = this->activeConnection(); + if (active && conn->path() == active->connection().path()) { + net->addActiveConnection(active); + } +} + +void NMWirelessDevice::onActiveConnectionLoaded(NMActiveConnection* active) { + QString activeConnPath = active->connection().path(); + for (auto* net: this->mBackendNetworks.values()) { + for (auto* conn: net->connections()) { + if (activeConnPath == conn->path()) { + net->addActiveConnection(active); + return; + } + } + } +} + +void NMWirelessDevice::scan() { + this->wirelessProxy->RequestScan({}); + this->bScanning = true; +} + +bool NMWirelessDevice::isWirelessValid() const { + return this->wirelessProxy && this->wirelessProxy->isValid(); +} + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/wireless.hpp b/src/network/nm/wireless.hpp new file mode 100644 index 00000000..2fa885f1 --- /dev/null +++ b/src/network/nm/wireless.hpp @@ -0,0 +1,150 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../wifi.hpp" +#include "accesspoint.hpp" +#include "connection.hpp" +#include "device.hpp" +#include "enums.hpp" +#include "nm/dbus_nm_wireless.h" + +namespace qs::dbus { +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMWirelessCapabilities::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus +namespace qs::network { + +// NMWirelessNetwork aggregates all related NMActiveConnection, NMAccessPoint, and NMConnectionSetting objects. +class NMWirelessNetwork: public QObject { + Q_OBJECT; + +public: + explicit NMWirelessNetwork(QString ssid, QObject* parent = nullptr); + void addAccessPoint(NMAccessPoint* ap); + void addConnectionSettings(NMConnectionSettings* conn); + void addActiveConnection(NMActiveConnection* active); + + [[nodiscard]] QString ssid() const { return this->mSsid; }; + [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; }; + [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; }; + [[nodiscard]] bool known() const { return this->bKnown; }; + [[nodiscard]] NMConnectionStateReason::Enum reason() const { return this->bReason; }; + [[nodiscard]] NMWirelessSecurityType::Enum security() const { return this->bSecurity; }; + [[nodiscard]] NMAccessPoint* referenceAp() const { return this->mReferenceAp; }; + [[nodiscard]] NMConnectionSettings* referenceConnection() const { return this->mReferenceConn; }; + [[nodiscard]] QList accessPoints() const { return this->mAccessPoints.values(); }; + [[nodiscard]] QList connections() const { return this->mConnections.values(); }; + [[nodiscard]] QBindable bindableActiveApPath() { return &this->bActiveApPath; }; + [[nodiscard]] QBindable bindableCapabilities() { return &this->bCaps; }; + +signals: + void signalStrengthChanged(quint8 signal); + void stateChanged(NMConnectionState::Enum state); + void knownChanged(bool known); + void reasonChanged(NMConnectionStateReason::Enum reason); + void securityChanged(NMWirelessSecurityType::Enum security); + void capabilitiesChanged(NMWirelessCapabilities::Enum caps); + void activeApPathChanged(QString path); + void disappeared(); + +private: + QString mSsid; + QHash mAccessPoints; + QHash mConnections; + NMActiveConnection* mActiveConnection = nullptr; + NMConnectionSettings* mReferenceConn = nullptr; + NMAccessPoint* mReferenceAp = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, bool, bKnown, &NMWirelessNetwork::knownChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMConnectionStateReason::Enum, bReason, &NMWirelessNetwork::reasonChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMConnectionState::Enum, bState, &NMWirelessNetwork::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMWirelessSecurityType::Enum, bSecurity, &NMWirelessNetwork::securityChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, quint8, bSignalStrength, &NMWirelessNetwork::signalStrengthChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMWirelessCapabilities::Enum, bCaps, &NMWirelessNetwork::capabilitiesChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, QString, bActiveApPath, &NMWirelessNetwork::activeApPathChanged); + // clang-format on + + void updateReferenceAp(); + void updateReferenceConnection(); + void removeAccessPoint(); + void removeConnectionSettings(); + void removeActiveConnection(); +}; + +// Proxy of a /org/freedesktop/NetworkManager/Device/* object. +// Extends NMDevice to also include members from the org.freedesktop.NetworkManager.Device.Wireless interface +// Owns the lifetime of NMAccessPoints(s), NMWirelessNetwork(s), frontend WifiNetwork(s). +class NMWirelessDevice: public NMDevice { + Q_OBJECT; + +public: + explicit NMWirelessDevice(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isWirelessValid() const; + [[nodiscard]] qint64 lastScan() { return this->bLastScan; }; + [[nodiscard]] NMWirelessCapabilities::Enum capabilities() { return this->bCapabilities; }; + [[nodiscard]] const QDBusObjectPath& activeApPath() { return this->bActiveAccessPoint; }; + [[nodiscard]] bool scanning() { return this->bScanning; }; + +public slots: + void scan(); + +signals: + void lastScanChanged(qint64 lastScan); + void scanningChanged(bool scanning); + void capabilitiesChanged(NMWirelessCapabilities::Enum caps); + void activeAccessPointChanged(const QDBusObjectPath& path); + void accessPointLoaded(NMAccessPoint* ap); + void accessPointRemoved(NMAccessPoint* ap); + void wifiNetworkAdded(WifiNetwork* net); + void wifiNetworkRemoved(WifiNetwork* net); + void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath); + void addAndActivateConnection( + const ConnectionSettingsMap& settings, + const QDBusObjectPath& devPath, + const QDBusObjectPath& apPath + ); + +private slots: + void onAccessPointPathAdded(const QDBusObjectPath& path); + void onAccessPointPathRemoved(const QDBusObjectPath& path); + void onAccessPointLoaded(NMAccessPoint* ap); + void onConnectionLoaded(NMConnectionSettings* conn); + void onActiveConnectionLoaded(NMActiveConnection* active); + +private: + void registerAccessPoint(const QString& path); + void registerAccessPoints(); + void initWireless(); + NMWirelessNetwork* registerNetwork(const QString& ssid); + + QHash mAccessPoints; + QHash mBackendNetworks; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, qint64, bLastScan, &NMWirelessDevice::lastScanChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, bool, bScanning, &NMWirelessDevice::scanningChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, NMWirelessCapabilities::Enum, bCapabilities, &NMWirelessDevice::capabilitiesChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, QDBusObjectPath, bActiveAccessPoint, &NMWirelessDevice::activeAccessPointChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMWireless, wirelessProperties); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pLastScan, bLastScan, wirelessProperties, "LastScan"); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pCapabilities, bCapabilities, wirelessProperties, "WirelessCapabilities"); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pActiveAccessPoint, bActiveAccessPoint, wirelessProperties, "ActiveAccessPoint"); + // clang-format on + + DBusNMWirelessProxy* wirelessProxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/test/network.qml b/src/network/test/network.qml new file mode 100644 index 00000000..e172222d --- /dev/null +++ b/src/network/test/network.qml @@ -0,0 +1,132 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Network + +FloatingWindow { + color: contentItem.palette.window + + ColumnLayout { + anchors.fill: parent + anchors.margins: 5 + + Column { + Layout.fillWidth: true + RowLayout { + Label { + text: "WiFi" + font.bold: true + font.pointSize: 12 + } + CheckBox { + text: "Software" + checked: Network.wifi.enabled + onClicked: Network.wifi.enabled = !Network.wifi.enabled + } + CheckBox { + enabled: false + text: "Hardware" + checked: Network.wifi.hardwareEnabled + } + } + } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + model: Network.wifi.devices + + delegate: WrapperRectangle { + width: parent.width + color: "transparent" + border.color: palette.button + border.width: 1 + margin: 5 + + property var sortedNetworks: { + return [...modelData.networks.values].sort((a, b) => { + if (a.connected !== b.connected) { + return b.connected - a.connected + } + return b.signalStrength - a.signalStrength + }) + } + + ColumnLayout { + Label { text: `Device: ${modelData.name} (${modelData.address})` } + RowLayout { + Label { + text: NetworkConnectionState.toString(modelData.state) + color: modelData.state == NetworkConnectionState.Connected ? palette.link : palette.placeholderText + } + Label { + visible: modelData.state == NetworkConnectionState.Connecting || modelData.state == NetworkConnectionState.Disconnecting + text: `(${NMDeviceState.toString(modelData.nmState)})` + } + Button { + visible: modelData.state == NetworkConnectionState.Connected + text: "Disconnect" + onClicked: modelData.disconnect() + } + Button { + text: "Scan" + onClicked: modelData.scan() + visible: modelData.scanning === false + } + } + + Repeater { + Layout.fillWidth: true + model: sortedNetworks + + WrapperRectangle { + Layout.fillWidth: true + color: modelData.connected ? "lightsteelblue" : palette.button + border.color: palette.mid + border.width: 1 + margin: 5 + + RowLayout { + ColumnLayout { + Layout.fillWidth: true + RowLayout { + Label { text: modelData.ssid; font.bold: true } + Label { + text: modelData.known ? "Known" : "" + color: palette.placeholderText + } + } + RowLayout { + Label { + text: `Security: ${NMWirelessSecurityType.toString(modelData.nmSecurity)}` + color: palette.placeholderText + } + Label { + text: `| Signal strength: ${modelData.signalStrength}%` + color: palette.placeholderText + } + } + Label { + visible: modelData.nmReason != NMConnectionStateReason.Unknown && modelData.nmReason != NMConnectionStateReason.None + text: `Connection change reason: ${NMConnectionStateReason.toString(modelData.nmReason)}` + } + } + ColumnLayout { + Layout.alignment: Qt.AlignRight + Button { + Layout.alignment: Qt.AlignRight + text: "Connect" + onClicked: modelData.connect() + visible: !modelData.connected + } + } + } + } + } + } + } + } + } +} diff --git a/src/network/wifi.cpp b/src/network/wifi.cpp new file mode 100644 index 00000000..fea6f4f8 --- /dev/null +++ b/src/network/wifi.cpp @@ -0,0 +1,80 @@ +#include "wifi.hpp" + +#include +#include + +#include "../core/logcat.hpp" + +namespace qs::network { + +namespace { +QS_LOGGING_CATEGORY(logWifiDevice, "quickshell.wifi.device", QtWarningMsg); +QS_LOGGING_CATEGORY(logWifiNetwork, "quickshell.wifi.network", QtWarningMsg); +QS_LOGGING_CATEGORY(logWifi, "quickshell.wifi", QtWarningMsg); +} // namespace + +WifiDevice::WifiDevice(QObject* parent): NetworkDevice(parent) {}; + +void WifiDevice::scan() { + if (this->bScanning) { + qCCritical(logWifiDevice) << this << "is already scanning"; + return; + } + qCDebug(logWifiDevice) << "Requesting scan on" << this; + this->requestScan(); +} + +void WifiDevice::networkAdded(WifiNetwork* net) { this->mNetworks.insertObject(net); } +void WifiDevice::networkRemoved(WifiNetwork* net) { this->mNetworks.removeObject(net); } + +WifiNetwork::WifiNetwork(QString ssid, QObject* parent): QObject(parent), mSsid(std::move(ssid)) {}; + +void WifiNetwork::connect() { + if (this->bConnected) { + qCCritical(logWifiNetwork) << this << "is already connected."; + return; + } + this->requestConnect(); +} + +Wifi::Wifi(QObject* parent): QObject(parent) {}; + +void Wifi::onDeviceAdded(WifiDevice* dev) { this->mDevices.insertObject(dev); } +void Wifi::onDeviceRemoved(WifiDevice* dev) { this->mDevices.removeObject(dev); } + +void Wifi::setEnabled(bool enabled) { + if (this->bEnabled == enabled) { + QString state = enabled ? "enabled" : "disabled"; + qCCritical(logWifi) << "Wifi is already" << state; + } else { + emit this->requestSetEnabled(enabled); + } +} + +} // namespace qs::network + +QDebug operator<<(QDebug debug, const qs::network::WifiDevice* device) { + auto saver = QDebugStateSaver(debug); + + if (device) { + debug.nospace() << "WifiDevice(" << static_cast(device) + << ", name=" << device->name() << ")"; + } else { + debug << "WifiDevice(nullptr)"; + } + + return debug; +} + +QDebug operator<<(QDebug debug, const qs::network::WifiNetwork* network) { + auto saver = QDebugStateSaver(debug); + + if (network) { + debug.nospace() << "WifiNetwork(" << static_cast(network) + << ", name=" << network->ssid() << ")"; + } else { + debug << "WifiDevice(nullptr)"; + } + + return debug; +} diff --git a/src/network/wifi.hpp b/src/network/wifi.hpp new file mode 100644 index 00000000..372d1605 --- /dev/null +++ b/src/network/wifi.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include +#include +#include +#include + +#include "../core/model.hpp" +#include "device.hpp" +#include "nm/enums.hpp" + +namespace qs::network { + +///! An available wifi network. +class WifiNetwork: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("WifiNetwork can only be acquired through WifiDevice"); + // clang-format off + /// The SSID (service set identifier) of the wifi network. + Q_PROPERTY(QString ssid READ ssid CONSTANT); + /// The current signal strength of the network, in percent. + Q_PROPERTY(quint8 signalStrength READ signalStrength NOTIFY signalStrengthChanged BINDABLE bindableSignalStrength); + /// True if the network is connected. + Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged BINDABLE bindableConnected); + /// True if the wifi network has known connection settings saved. + Q_PROPERTY(bool known READ known NOTIFY knownChanged BINDABLE bindableKnown); + /// A specific reason for the connection state when the backend is NetworkManager. + Q_PROPERTY(NMConnectionStateReason::Enum nmReason READ nmReason NOTIFY nmReasonChanged BINDABLE bindableNmReason); + /// The security type of the wifi network when the backend is NetworkManager. + Q_PROPERTY(NMWirelessSecurityType::Enum nmSecurity READ nmSecurity NOTIFY nmSecurityChanged BINDABLE bindableNmSecurity); + // clang-format on + +signals: + void signalStrengthChanged(); + void connectedChanged(); + void knownChanged(); + void nmReasonChanged(); + void nmSecurityChanged(); + void requestConnect(); + +public: + explicit WifiNetwork(QString ssid, QObject* parent = nullptr); + + /// Attempt to connect to the wifi network. + Q_INVOKABLE void connect(); + + [[nodiscard]] QString ssid() const { return this->mSsid; }; + [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; }; + [[nodiscard]] bool connected() const { return this->bConnected; }; + [[nodiscard]] bool known() const { return this->bKnown; }; + [[nodiscard]] NMConnectionStateReason::Enum nmReason() const { return this->bNmReason; }; + [[nodiscard]] NMWirelessSecurityType::Enum nmSecurity() const { return this->bNmSecurity; }; + QBindable bindableSignalStrength() { return &this->bSignalStrength; } + QBindable bindableConnected() { return &this->bConnected; } + QBindable bindableKnown() { return &this->bKnown; } + QBindable bindableNmReason() { return &this->bNmReason; } + QBindable bindableNmSecurity() { return &this->bNmSecurity; } + +private: + QString mSsid; + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, quint8, bSignalStrength, &WifiNetwork::signalStrengthChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, bool, bConnected, &WifiNetwork::connectedChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, bool, bKnown, &WifiNetwork::knownChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, NMConnectionStateReason::Enum, bNmReason, &WifiNetwork::nmReasonChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, NMWirelessSecurityType::Enum, bNmSecurity, &WifiNetwork::nmSecurityChanged); + // clang-format on +}; + +///! Wireless variant of a network device. +class WifiDevice: public NetworkDevice { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("WifiDevices can only be acquired through Wifi"); + + /// True if the wifi device is currently scanning for available wifi networks. + Q_PROPERTY(bool scanning READ scanning NOTIFY scanningChanged); + /// A list of all wifi networks currently available. + Q_PROPERTY(UntypedObjectModel* networks READ networks CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*) + +signals: + void requestScan(); + void scanningChanged(); + +public slots: + void networkAdded(WifiNetwork* net); + void networkRemoved(WifiNetwork* net); + +public: + explicit WifiDevice(QObject* parent = nullptr); + + /// Request the wireless device to scan for available WiFi networks. + /// This should be invoked everytime you want to show the user an accurate list of available networks. + Q_INVOKABLE void scan(); + + [[nodiscard]] ObjectModel* networks() { return &this->mNetworks; }; + [[nodiscard]] bool scanning() const { return this->bScanning; }; + QBindable bindableScanning() { return &this->bScanning; }; + +private: + ObjectModel mNetworks {this}; + Q_OBJECT_BINDABLE_PROPERTY(WifiDevice, bool, bScanning, &WifiDevice::scanningChanged); +}; + +///! A manager for all wifi state and devices. +class Wifi: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("Wifi can only be acquired through Network"); + // clang-format off + /// A list of all wifi devices. + Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + /// True when the wifi software switch is enabled. + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); + /// True when the wifi hardware switch is enabled. + Q_PROPERTY(bool hardwareEnabled READ hardwareEnabled NOTIFY hardwareEnabledChanged BINDABLE bindableHardwareEnabled); + // clang-format on + +signals: + void enabledChanged(); + void hardwareEnabledChanged(); + void defaultDeviceChanged(); + void requestSetEnabled(bool enabled); + +public slots: + void onDeviceAdded(WifiDevice* dev); + void onDeviceRemoved(WifiDevice* dev); + +public: + explicit Wifi(QObject* parent = nullptr); + + [[nodiscard]] ObjectModel* devices() { return &this->mDevices; }; + [[nodiscard]] bool enabled() const { return this->bEnabled; }; + void setEnabled(bool enabled); + [[nodiscard]] bool hardwareEnabled() const { return this->bHardwareEnabled; }; + QBindable bindableEnabled() { return &this->bEnabled; }; + QBindable bindableHardwareEnabled() { return &this->bHardwareEnabled; }; + +private: + ObjectModel mDevices {this}; + Q_OBJECT_BINDABLE_PROPERTY(Wifi, bool, bEnabled, &Wifi::enabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(Wifi, bool, bHardwareEnabled, &Wifi::hardwareEnabledChanged); +}; + +}; // namespace qs::network + +QDebug operator<<(QDebug debug, const qs::network::WifiDevice* device); +QDebug operator<<(QDebug debug, const qs::network::WifiNetwork* network);