diff --git a/linux/BluetoothMonitor.cpp b/linux/BluetoothMonitor.cpp new file mode 100644 index 0000000..66501f9 --- /dev/null +++ b/linux/BluetoothMonitor.cpp @@ -0,0 +1,93 @@ +#include "BluetoothMonitor.h" +#include "logger.h" + +#include + +BluetoothMonitor::BluetoothMonitor(QObject *parent) + : QObject(parent), m_dbus(QDBusConnection::systemBus()) +{ + if (!m_dbus.isConnected()) + { + LOG_WARN("Failed to connect to system D-Bus"); + return; + } + + registerDBusService(); +} + +BluetoothMonitor::~BluetoothMonitor() +{ + m_dbus.disconnectFromBus(m_dbus.name()); +} + +void BluetoothMonitor::registerDBusService() +{ + // Match signals for PropertiesChanged on any BlueZ Device interface + QString matchRule = QStringLiteral("type='signal'," + "interface='org.freedesktop.DBus.Properties'," + "member='PropertiesChanged'," + "path_namespace='/org/bluez'"); + + m_dbus.connect("org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "AddMatch", + this, + SLOT(onPropertiesChanged(QString, QVariantMap, QStringList))); + + if (!m_dbus.connect("", "", "org.freedesktop.DBus.Properties", "PropertiesChanged", + this, SLOT(onPropertiesChanged(QString, QVariantMap, QStringList)))) + { + LOG_WARN("Failed to connect to D-Bus PropertiesChanged signal"); + } +} + +void BluetoothMonitor::onPropertiesChanged(const QString &interface, const QVariantMap &changedProps, const QStringList &invalidatedProps) +{ + Q_UNUSED(invalidatedProps); + + if (interface != "org.bluez.Device1") + { + return; + } + + if (changedProps.contains("Connected")) + { + bool connected = changedProps["Connected"].toBool(); + QString path = QDBusContext::message().path(); + + QDBusInterface deviceInterface("org.bluez", path, "org.freedesktop.DBus.Properties", m_dbus); + + // Get the device address + QDBusReply addrReply = deviceInterface.call("Get", "org.bluez.Device1", "Address"); + if (!addrReply.isValid()) + { + return; + } + QString macAddress = addrReply.value().toString(); + + // Get UUIDs to check if it's an AirPods device + QDBusReply uuidsReply = deviceInterface.call("Get", "org.bluez.Device1", "UUIDs"); + if (!uuidsReply.isValid()) + { + return; + } + + QStringList uuids = uuidsReply.value().toStringList(); + if (!uuids.contains("74ec2172-0bad-4d01-8f77-997b2be0722a")) + { + return; // Not an AirPods device + } + + if (connected) + { + emit deviceConnected(macAddress); + LOG_DEBUG("AirPods device connected:" << macAddress); + } + else + { + emit deviceDisconnected(macAddress); + LOG_DEBUG("AirPods device disconnected:" << macAddress); + } + } +} \ No newline at end of file diff --git a/linux/BluetoothMonitor.h b/linux/BluetoothMonitor.h new file mode 100644 index 0000000..c91a32c --- /dev/null +++ b/linux/BluetoothMonitor.h @@ -0,0 +1,26 @@ +#ifndef BLUETOOTHMONITOR_H +#define BLUETOOTHMONITOR_H + +#include +#include + +class BluetoothMonitor : public QObject, protected QDBusContext +{ + Q_OBJECT +public: + explicit BluetoothMonitor(QObject *parent = nullptr); + ~BluetoothMonitor(); + +signals: + void deviceConnected(const QString &macAddress); + void deviceDisconnected(const QString &macAddress); + +private slots: + void onPropertiesChanged(const QString &interface, const QVariantMap &changedProps, const QStringList &invalidatedProps); + +private: + QDBusConnection m_dbus; + void registerDBusService(); +}; + +#endif // BLUETOOTHMONITOR_H \ No newline at end of file diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 7636e46..ce8d1ab 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -19,6 +19,8 @@ qt_add_executable(applinux trayiconmanager.h enums.h battery.hpp + BluetoothMonitor.cpp + BluetoothMonitor.h ) qt_add_qml_module(applinux diff --git a/linux/SegmentedControl.qml b/linux/SegmentedControl.qml index 77b0a4c..b959e37 100644 --- a/linux/SegmentedControl.qml +++ b/linux/SegmentedControl.qml @@ -53,6 +53,18 @@ Control { height: root.availableHeight focusPolicy: Qt.NoFocus // Let the root control handle focus + // Add explicit text color + contentItem: Text { + text: segmentButton.text + font: segmentButton.font + color: root.currentIndex === segmentButton.index ? root.selectedTextColor : root.textColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + leftPadding: 2 + rightPadding: 2 + elide: Text.ElideRight + } + background: Rectangle { radius: height / 2 color: root.currentIndex === segmentButton.index ? root.selectedColor : "transparent" diff --git a/linux/main.cpp b/linux/main.cpp index cf2f59b..edef857 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -7,6 +7,7 @@ #include "trayiconmanager.h" #include "enums.h" #include "battery.hpp" +#include "BluetoothMonitor.h" using namespace AirpodsTrayApp::Enums; @@ -31,7 +32,8 @@ class AirPodsTrayApp : public QObject { public: AirPodsTrayApp(bool debugMode) : debugMode(debugMode) - , m_battery(new Battery(this)) { + , m_battery(new Battery(this)) + , monitor(new BluetoothMonitor(this)) { if (debugMode) { QLoggingCategory::setFilterRules("airpodsApp.debug=true"); } else { @@ -55,6 +57,9 @@ public: mediaController->initializeMprisInterface(); mediaController->followMediaChanges(); + connect(monitor, &BluetoothMonitor::deviceConnected, this, &AirPodsTrayApp::bluezDeviceConnected); + connect(monitor, &BluetoothMonitor::deviceDisconnected, this, &AirPodsTrayApp::bluezDeviceDisconnected); + connect(m_battery, &Battery::primaryChanged, this, &AirPodsTrayApp::primaryChanged); CrossDevice.isEnabled = loadCrossDeviceEnabled(); @@ -78,15 +83,6 @@ public: } } - QDBusInterface iface("org.bluez", "/org/bluez", "org.bluez.Adapter1"); - QDBusReply reply = iface.call("GetServiceRecords", QString::fromUtf8("74ec2172-0bad-4d01-8f77-997b2be0722a")); - if (reply.isValid()) { - LOG_INFO("Service record found, proceeding with connection"); - } else { - LOG_WARN("Service record not found, waiting for BLE broadcast"); - } - - listenForDeviceConnections(); initializeDBus(); initializeBluetooth(); } @@ -97,8 +93,6 @@ public: delete trayIcon; delete trayMenu; delete discoveryAgent; - delete bluezInterface; - delete mprisInterface; delete socket; delete phoneSocket; } @@ -137,46 +131,7 @@ private: bool isEnabled = true; // Ability to disable the feature } CrossDevice; - void initializeDBus() { - QDBusConnection systemBus = QDBusConnection::systemBus(); - if (!systemBus.isConnected()) { - } - - bluezInterface = new QDBusInterface("org.bluez", - "/", - "org.freedesktop.DBus.ObjectManager", - systemBus, - this); - - if (!bluezInterface->isValid()) { - LOG_ERROR("Failed to connect to org.bluez DBus interface."); - return; - } - - connect(systemBus.interface(), &QDBusConnectionInterface::NameOwnerChanged, - this, &AirPodsTrayApp::onNameOwnerChanged); - - systemBus.connect(QString(), QString(), "org.freedesktop.DBus.Properties", "PropertiesChanged", - this, SLOT(onDevicePropertiesChanged(QString, QVariantMap, QStringList))); - - systemBus.connect(QString(), QString(), "org.freedesktop.DBus.ObjectManager", "InterfacesAdded", - this, SLOT(onInterfacesAdded(QString, QVariantMap))); - - QDBusMessage msg = bluezInterface->call("GetManagedObjects"); - if (msg.type() == QDBusMessage::ErrorMessage) { - LOG_ERROR("Error getting managed objects: " << msg.errorMessage()); - return; - } - - QVariantMap objects = qdbus_cast(msg.arguments().at(0)); - for (auto it = objects.begin(); it != objects.end(); ++it) { - if (it.key().startsWith("/org/bluez/hci0/dev_")) { - LOG_INFO("Existing device: " << it.key()); - } - } - QDBusConnection::systemBus().registerObject("/me/kavishdevar/aln", this); - QDBusConnection::systemBus().registerService("me.kavishdevar.aln"); - } + void initializeDBus() { } bool isAirPodsDevice(const QBluetoothDeviceInfo &device) { @@ -195,43 +150,11 @@ private: LOG_WARN("Phone socket is not open, cannot send notification packet"); } } - void onNameOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner) { - if (name == "org.bluez") { - if (newOwner.isEmpty()) { - LOG_WARN("BlueZ has been stopped."); - } else { - LOG_INFO("BlueZ started."); - } - } - } - - void onDevicePropertiesChanged(const QString &interface, const QVariantMap &changed, const QStringList &invalidated) { - if (interface != "org.bluez.Device1") - return; - - if (changed.contains("Connected")) { - bool connected = changed.value("Connected").toBool(); - QString devicePath = sender()->objectName(); - LOG_INFO(QString("Device %1 connected: %2").arg(devicePath, connected ? "Yes" : "No")); - - if (connected) { - const QBluetoothAddress address = QBluetoothAddress(devicePath.split("/").last().replace("_", ":")); - QBluetoothDeviceInfo device(address, "", 0); - if (isAirPodsDevice(device)) { - connectToDevice(device); - } - } else { - disconnectDevice(devicePath); - } - } - } void disconnectDevice(const QString &devicePath) { LOG_INFO("Disconnecting device at " << devicePath); } - QDBusInterface *bluezInterface = nullptr; - public slots: void connectToDevice(const QString &address) { LOG_INFO("Connecting to device with address: " << address); @@ -422,6 +345,11 @@ private slots: connectToDevice(device); } } + void bluezDeviceConnected(const QString &address) + { + QBluetoothDeviceInfo device(QBluetoothAddress(address), "", 0); + connectToDevice(device); + } void onDeviceDisconnected(const QBluetoothAddress &address) { @@ -431,6 +359,7 @@ private slots: LOG_WARN("Socket is still open, closing it"); socket->close(); socket = nullptr; + discoveryAgent->start(); } if (phoneSocket && phoneSocket->isOpen()) { @@ -438,6 +367,15 @@ private slots: LOG_DEBUG("AIRPODS_DISCONNECTED packet written: " << AirPodsPackets::Connection::AIRPODS_DISCONNECTED.toHex()); } } + void bluezDeviceDisconnected(const QString &address) + { + if (address == connectedDeviceMacAddress.replace("_", ":")) { + onDeviceDisconnected(QBluetoothAddress(address)); + } + else { + LOG_WARN("Disconnected device does not match connected device: " << address << " != " << connectedDeviceMacAddress); + } + } void parseMetadata(const QByteArray &data) { @@ -524,9 +462,6 @@ private slots: LOG_INFO("Connecting to device: " << device.name()); QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol); - connect(localSocket, &QBluetoothSocket::disconnected, this, [this, localSocket]() { - onDeviceDisconnected(localSocket->peerAddress()); - }); connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() { // Start periodic magic pairing attempts QTimer *magicPairingTimer = new QTimer(this); @@ -778,26 +713,6 @@ private slots: QMetaObject::invokeMethod(this, "handlePhonePacket", Qt::QueuedConnection, Q_ARG(QByteArray, data)); } - void listenForDeviceConnections() { - QDBusConnection systemBus = QDBusConnection::systemBus(); - systemBus.connect(QString(), QString(), "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(onDevicePropertiesChanged(QString, QVariantMap, QStringList))); - systemBus.connect(QString(), QString(), "org.freedesktop.DBus.ObjectManager", "InterfacesAdded", this, SLOT(onInterfacesAdded(QString, QVariantMap))); - } - - void onInterfacesAdded(QString path, QVariantMap interfaces) { - if (interfaces.contains("org.bluez.Device1")) { - QVariantMap deviceProps = interfaces["org.bluez.Device1"].toMap(); - if (deviceProps.contains("Connected") && deviceProps["Connected"].toBool()) { - QString addr = deviceProps["Address"].toString(); - QBluetoothAddress btAddress(addr); - QBluetoothDeviceInfo device(btAddress, "", 0); - if (isAirPodsDevice(device)) { - connectToDevice(device); - } - } - } - } - public: void handleMediaStateChange(MediaController::MediaState state) { if (state == MediaController::MediaState::Playing) { @@ -903,12 +818,12 @@ private: QBluetoothDeviceDiscoveryAgent *discoveryAgent; QBluetoothSocket *socket = nullptr; QBluetoothSocket *phoneSocket = nullptr; - QDBusInterface *mprisInterface; QString connectedDeviceMacAddress; QByteArray lastBatteryStatus; QByteArray lastEarDetectionStatus; MediaController* mediaController; TrayIconManager *trayManager; + BluetoothMonitor *monitor; QSettings *settings; QString m_batteryStatus;