Skip to content

Commit e384840

Browse files
authored
[Linux] Implement proper disconnection detection (#95)
* [Linux] Implement proper disconection detection * Only trigger on airpod devices * Remove unused code * [Linux] Fix SegmentedControl text not shown in release build (but debug builds showed text)???
1 parent b181177 commit e384840

5 files changed

Lines changed: 156 additions & 108 deletions

File tree

linux/BluetoothMonitor.cpp

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#include "BluetoothMonitor.h"
2+
#include "logger.h"
3+
4+
#include <QDebug>
5+
6+
BluetoothMonitor::BluetoothMonitor(QObject *parent)
7+
: QObject(parent), m_dbus(QDBusConnection::systemBus())
8+
{
9+
if (!m_dbus.isConnected())
10+
{
11+
LOG_WARN("Failed to connect to system D-Bus");
12+
return;
13+
}
14+
15+
registerDBusService();
16+
}
17+
18+
BluetoothMonitor::~BluetoothMonitor()
19+
{
20+
m_dbus.disconnectFromBus(m_dbus.name());
21+
}
22+
23+
void BluetoothMonitor::registerDBusService()
24+
{
25+
// Match signals for PropertiesChanged on any BlueZ Device interface
26+
QString matchRule = QStringLiteral("type='signal',"
27+
"interface='org.freedesktop.DBus.Properties',"
28+
"member='PropertiesChanged',"
29+
"path_namespace='/org/bluez'");
30+
31+
m_dbus.connect("org.freedesktop.DBus",
32+
"/org/freedesktop/DBus",
33+
"org.freedesktop.DBus",
34+
"AddMatch",
35+
this,
36+
SLOT(onPropertiesChanged(QString, QVariantMap, QStringList)));
37+
38+
if (!m_dbus.connect("", "", "org.freedesktop.DBus.Properties", "PropertiesChanged",
39+
this, SLOT(onPropertiesChanged(QString, QVariantMap, QStringList))))
40+
{
41+
LOG_WARN("Failed to connect to D-Bus PropertiesChanged signal");
42+
}
43+
}
44+
45+
void BluetoothMonitor::onPropertiesChanged(const QString &interface, const QVariantMap &changedProps, const QStringList &invalidatedProps)
46+
{
47+
Q_UNUSED(invalidatedProps);
48+
49+
if (interface != "org.bluez.Device1")
50+
{
51+
return;
52+
}
53+
54+
if (changedProps.contains("Connected"))
55+
{
56+
bool connected = changedProps["Connected"].toBool();
57+
QString path = QDBusContext::message().path();
58+
59+
QDBusInterface deviceInterface("org.bluez", path, "org.freedesktop.DBus.Properties", m_dbus);
60+
61+
// Get the device address
62+
QDBusReply<QVariant> addrReply = deviceInterface.call("Get", "org.bluez.Device1", "Address");
63+
if (!addrReply.isValid())
64+
{
65+
return;
66+
}
67+
QString macAddress = addrReply.value().toString();
68+
69+
// Get UUIDs to check if it's an AirPods device
70+
QDBusReply<QVariant> uuidsReply = deviceInterface.call("Get", "org.bluez.Device1", "UUIDs");
71+
if (!uuidsReply.isValid())
72+
{
73+
return;
74+
}
75+
76+
QStringList uuids = uuidsReply.value().toStringList();
77+
if (!uuids.contains("74ec2172-0bad-4d01-8f77-997b2be0722a"))
78+
{
79+
return; // Not an AirPods device
80+
}
81+
82+
if (connected)
83+
{
84+
emit deviceConnected(macAddress);
85+
LOG_DEBUG("AirPods device connected:" << macAddress);
86+
}
87+
else
88+
{
89+
emit deviceDisconnected(macAddress);
90+
LOG_DEBUG("AirPods device disconnected:" << macAddress);
91+
}
92+
}
93+
}

linux/BluetoothMonitor.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#ifndef BLUETOOTHMONITOR_H
2+
#define BLUETOOTHMONITOR_H
3+
4+
#include <QObject>
5+
#include <QtDBus/QtDBus>
6+
7+
class BluetoothMonitor : public QObject, protected QDBusContext
8+
{
9+
Q_OBJECT
10+
public:
11+
explicit BluetoothMonitor(QObject *parent = nullptr);
12+
~BluetoothMonitor();
13+
14+
signals:
15+
void deviceConnected(const QString &macAddress);
16+
void deviceDisconnected(const QString &macAddress);
17+
18+
private slots:
19+
void onPropertiesChanged(const QString &interface, const QVariantMap &changedProps, const QStringList &invalidatedProps);
20+
21+
private:
22+
QDBusConnection m_dbus;
23+
void registerDBusService();
24+
};
25+
26+
#endif // BLUETOOTHMONITOR_H

linux/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ qt_add_executable(applinux
1919
trayiconmanager.h
2020
enums.h
2121
battery.hpp
22+
BluetoothMonitor.cpp
23+
BluetoothMonitor.h
2224
)
2325

2426
qt_add_qml_module(applinux

linux/SegmentedControl.qml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ Control {
5353
height: root.availableHeight
5454
focusPolicy: Qt.NoFocus // Let the root control handle focus
5555

56+
// Add explicit text color
57+
contentItem: Text {
58+
text: segmentButton.text
59+
font: segmentButton.font
60+
color: root.currentIndex === segmentButton.index ? root.selectedTextColor : root.textColor
61+
horizontalAlignment: Text.AlignHCenter
62+
verticalAlignment: Text.AlignVCenter
63+
leftPadding: 2
64+
rightPadding: 2
65+
elide: Text.ElideRight
66+
}
67+
5668
background: Rectangle {
5769
radius: height / 2
5870
color: root.currentIndex === segmentButton.index ? root.selectedColor : "transparent"

linux/main.cpp

Lines changed: 23 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "trayiconmanager.h"
88
#include "enums.h"
99
#include "battery.hpp"
10+
#include "BluetoothMonitor.h"
1011

1112
using namespace AirpodsTrayApp::Enums;
1213

@@ -31,7 +32,8 @@ class AirPodsTrayApp : public QObject {
3132
public:
3233
AirPodsTrayApp(bool debugMode)
3334
: debugMode(debugMode)
34-
, m_battery(new Battery(this)) {
35+
, m_battery(new Battery(this))
36+
, monitor(new BluetoothMonitor(this)) {
3537
if (debugMode) {
3638
QLoggingCategory::setFilterRules("airpodsApp.debug=true");
3739
} else {
@@ -55,6 +57,9 @@ class AirPodsTrayApp : public QObject {
5557
mediaController->initializeMprisInterface();
5658
mediaController->followMediaChanges();
5759

60+
connect(monitor, &BluetoothMonitor::deviceConnected, this, &AirPodsTrayApp::bluezDeviceConnected);
61+
connect(monitor, &BluetoothMonitor::deviceDisconnected, this, &AirPodsTrayApp::bluezDeviceDisconnected);
62+
5863
connect(m_battery, &Battery::primaryChanged, this, &AirPodsTrayApp::primaryChanged);
5964

6065
CrossDevice.isEnabled = loadCrossDeviceEnabled();
@@ -78,15 +83,6 @@ class AirPodsTrayApp : public QObject {
7883
}
7984
}
8085

81-
QDBusInterface iface("org.bluez", "/org/bluez", "org.bluez.Adapter1");
82-
QDBusReply<QVariant> reply = iface.call("GetServiceRecords", QString::fromUtf8("74ec2172-0bad-4d01-8f77-997b2be0722a"));
83-
if (reply.isValid()) {
84-
LOG_INFO("Service record found, proceeding with connection");
85-
} else {
86-
LOG_WARN("Service record not found, waiting for BLE broadcast");
87-
}
88-
89-
listenForDeviceConnections();
9086
initializeDBus();
9187
initializeBluetooth();
9288
}
@@ -97,8 +93,6 @@ class AirPodsTrayApp : public QObject {
9793
delete trayIcon;
9894
delete trayMenu;
9995
delete discoveryAgent;
100-
delete bluezInterface;
101-
delete mprisInterface;
10296
delete socket;
10397
delete phoneSocket;
10498
}
@@ -137,46 +131,7 @@ class AirPodsTrayApp : public QObject {
137131
bool isEnabled = true; // Ability to disable the feature
138132
} CrossDevice;
139133

140-
void initializeDBus() {
141-
QDBusConnection systemBus = QDBusConnection::systemBus();
142-
if (!systemBus.isConnected()) {
143-
}
144-
145-
bluezInterface = new QDBusInterface("org.bluez",
146-
"/",
147-
"org.freedesktop.DBus.ObjectManager",
148-
systemBus,
149-
this);
150-
151-
if (!bluezInterface->isValid()) {
152-
LOG_ERROR("Failed to connect to org.bluez DBus interface.");
153-
return;
154-
}
155-
156-
connect(systemBus.interface(), &QDBusConnectionInterface::NameOwnerChanged,
157-
this, &AirPodsTrayApp::onNameOwnerChanged);
158-
159-
systemBus.connect(QString(), QString(), "org.freedesktop.DBus.Properties", "PropertiesChanged",
160-
this, SLOT(onDevicePropertiesChanged(QString, QVariantMap, QStringList)));
161-
162-
systemBus.connect(QString(), QString(), "org.freedesktop.DBus.ObjectManager", "InterfacesAdded",
163-
this, SLOT(onInterfacesAdded(QString, QVariantMap)));
164-
165-
QDBusMessage msg = bluezInterface->call("GetManagedObjects");
166-
if (msg.type() == QDBusMessage::ErrorMessage) {
167-
LOG_ERROR("Error getting managed objects: " << msg.errorMessage());
168-
return;
169-
}
170-
171-
QVariantMap objects = qdbus_cast<QVariantMap>(msg.arguments().at(0));
172-
for (auto it = objects.begin(); it != objects.end(); ++it) {
173-
if (it.key().startsWith("/org/bluez/hci0/dev_")) {
174-
LOG_INFO("Existing device: " << it.key());
175-
}
176-
}
177-
QDBusConnection::systemBus().registerObject("/me/kavishdevar/aln", this);
178-
QDBusConnection::systemBus().registerService("me.kavishdevar.aln");
179-
}
134+
void initializeDBus() { }
180135

181136
bool isAirPodsDevice(const QBluetoothDeviceInfo &device)
182137
{
@@ -195,43 +150,11 @@ class AirPodsTrayApp : public QObject {
195150
LOG_WARN("Phone socket is not open, cannot send notification packet");
196151
}
197152
}
198-
void onNameOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner) {
199-
if (name == "org.bluez") {
200-
if (newOwner.isEmpty()) {
201-
LOG_WARN("BlueZ has been stopped.");
202-
} else {
203-
LOG_INFO("BlueZ started.");
204-
}
205-
}
206-
}
207-
208-
void onDevicePropertiesChanged(const QString &interface, const QVariantMap &changed, const QStringList &invalidated) {
209-
if (interface != "org.bluez.Device1")
210-
return;
211-
212-
if (changed.contains("Connected")) {
213-
bool connected = changed.value("Connected").toBool();
214-
QString devicePath = sender()->objectName();
215-
LOG_INFO(QString("Device %1 connected: %2").arg(devicePath, connected ? "Yes" : "No"));
216-
217-
if (connected) {
218-
const QBluetoothAddress address = QBluetoothAddress(devicePath.split("/").last().replace("_", ":"));
219-
QBluetoothDeviceInfo device(address, "", 0);
220-
if (isAirPodsDevice(device)) {
221-
connectToDevice(device);
222-
}
223-
} else {
224-
disconnectDevice(devicePath);
225-
}
226-
}
227-
}
228153

229154
void disconnectDevice(const QString &devicePath) {
230155
LOG_INFO("Disconnecting device at " << devicePath);
231156
}
232157

233-
QDBusInterface *bluezInterface = nullptr;
234-
235158
public slots:
236159
void connectToDevice(const QString &address) {
237160
LOG_INFO("Connecting to device with address: " << address);
@@ -422,6 +345,11 @@ private slots:
422345
connectToDevice(device);
423346
}
424347
}
348+
void bluezDeviceConnected(const QString &address)
349+
{
350+
QBluetoothDeviceInfo device(QBluetoothAddress(address), "", 0);
351+
connectToDevice(device);
352+
}
425353

426354
void onDeviceDisconnected(const QBluetoothAddress &address)
427355
{
@@ -431,13 +359,23 @@ private slots:
431359
LOG_WARN("Socket is still open, closing it");
432360
socket->close();
433361
socket = nullptr;
362+
discoveryAgent->start();
434363
}
435364
if (phoneSocket && phoneSocket->isOpen())
436365
{
437366
phoneSocket->write(AirPodsPackets::Connection::AIRPODS_DISCONNECTED);
438367
LOG_DEBUG("AIRPODS_DISCONNECTED packet written: " << AirPodsPackets::Connection::AIRPODS_DISCONNECTED.toHex());
439368
}
440369
}
370+
void bluezDeviceDisconnected(const QString &address)
371+
{
372+
if (address == connectedDeviceMacAddress.replace("_", ":")) {
373+
onDeviceDisconnected(QBluetoothAddress(address));
374+
}
375+
else {
376+
LOG_WARN("Disconnected device does not match connected device: " << address << " != " << connectedDeviceMacAddress);
377+
}
378+
}
441379

442380
void parseMetadata(const QByteArray &data)
443381
{
@@ -524,9 +462,6 @@ private slots:
524462

525463
LOG_INFO("Connecting to device: " << device.name());
526464
QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol);
527-
connect(localSocket, &QBluetoothSocket::disconnected, this, [this, localSocket]() {
528-
onDeviceDisconnected(localSocket->peerAddress());
529-
});
530465
connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() {
531466
// Start periodic magic pairing attempts
532467
QTimer *magicPairingTimer = new QTimer(this);
@@ -778,26 +713,6 @@ private slots:
778713
QMetaObject::invokeMethod(this, "handlePhonePacket", Qt::QueuedConnection, Q_ARG(QByteArray, data));
779714
}
780715

781-
void listenForDeviceConnections() {
782-
QDBusConnection systemBus = QDBusConnection::systemBus();
783-
systemBus.connect(QString(), QString(), "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(onDevicePropertiesChanged(QString, QVariantMap, QStringList)));
784-
systemBus.connect(QString(), QString(), "org.freedesktop.DBus.ObjectManager", "InterfacesAdded", this, SLOT(onInterfacesAdded(QString, QVariantMap)));
785-
}
786-
787-
void onInterfacesAdded(QString path, QVariantMap interfaces) {
788-
if (interfaces.contains("org.bluez.Device1")) {
789-
QVariantMap deviceProps = interfaces["org.bluez.Device1"].toMap();
790-
if (deviceProps.contains("Connected") && deviceProps["Connected"].toBool()) {
791-
QString addr = deviceProps["Address"].toString();
792-
QBluetoothAddress btAddress(addr);
793-
QBluetoothDeviceInfo device(btAddress, "", 0);
794-
if (isAirPodsDevice(device)) {
795-
connectToDevice(device);
796-
}
797-
}
798-
}
799-
}
800-
801716
public:
802717
void handleMediaStateChange(MediaController::MediaState state) {
803718
if (state == MediaController::MediaState::Playing) {
@@ -903,12 +818,12 @@ private slots:
903818
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
904819
QBluetoothSocket *socket = nullptr;
905820
QBluetoothSocket *phoneSocket = nullptr;
906-
QDBusInterface *mprisInterface;
907821
QString connectedDeviceMacAddress;
908822
QByteArray lastBatteryStatus;
909823
QByteArray lastEarDetectionStatus;
910824
MediaController* mediaController;
911825
TrayIconManager *trayManager;
826+
BluetoothMonitor *monitor;
912827
QSettings *settings;
913828

914829
QString m_batteryStatus;

0 commit comments

Comments
 (0)