mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-28 22:01:50 +00:00
[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)???
This commit is contained in:
93
linux/BluetoothMonitor.cpp
Normal file
93
linux/BluetoothMonitor.cpp
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#include "BluetoothMonitor.h"
|
||||||
|
#include "logger.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
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<QVariant> 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<QVariant> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
linux/BluetoothMonitor.h
Normal file
26
linux/BluetoothMonitor.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#ifndef BLUETOOTHMONITOR_H
|
||||||
|
#define BLUETOOTHMONITOR_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QtDBus/QtDBus>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -19,6 +19,8 @@ qt_add_executable(applinux
|
|||||||
trayiconmanager.h
|
trayiconmanager.h
|
||||||
enums.h
|
enums.h
|
||||||
battery.hpp
|
battery.hpp
|
||||||
|
BluetoothMonitor.cpp
|
||||||
|
BluetoothMonitor.h
|
||||||
)
|
)
|
||||||
|
|
||||||
qt_add_qml_module(applinux
|
qt_add_qml_module(applinux
|
||||||
|
|||||||
@@ -53,6 +53,18 @@ Control {
|
|||||||
height: root.availableHeight
|
height: root.availableHeight
|
||||||
focusPolicy: Qt.NoFocus // Let the root control handle focus
|
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 {
|
background: Rectangle {
|
||||||
radius: height / 2
|
radius: height / 2
|
||||||
color: root.currentIndex === segmentButton.index ? root.selectedColor : "transparent"
|
color: root.currentIndex === segmentButton.index ? root.selectedColor : "transparent"
|
||||||
|
|||||||
131
linux/main.cpp
131
linux/main.cpp
@@ -7,6 +7,7 @@
|
|||||||
#include "trayiconmanager.h"
|
#include "trayiconmanager.h"
|
||||||
#include "enums.h"
|
#include "enums.h"
|
||||||
#include "battery.hpp"
|
#include "battery.hpp"
|
||||||
|
#include "BluetoothMonitor.h"
|
||||||
|
|
||||||
using namespace AirpodsTrayApp::Enums;
|
using namespace AirpodsTrayApp::Enums;
|
||||||
|
|
||||||
@@ -31,7 +32,8 @@ class AirPodsTrayApp : public QObject {
|
|||||||
public:
|
public:
|
||||||
AirPodsTrayApp(bool debugMode)
|
AirPodsTrayApp(bool debugMode)
|
||||||
: debugMode(debugMode)
|
: debugMode(debugMode)
|
||||||
, m_battery(new Battery(this)) {
|
, m_battery(new Battery(this))
|
||||||
|
, monitor(new BluetoothMonitor(this)) {
|
||||||
if (debugMode) {
|
if (debugMode) {
|
||||||
QLoggingCategory::setFilterRules("airpodsApp.debug=true");
|
QLoggingCategory::setFilterRules("airpodsApp.debug=true");
|
||||||
} else {
|
} else {
|
||||||
@@ -55,6 +57,9 @@ public:
|
|||||||
mediaController->initializeMprisInterface();
|
mediaController->initializeMprisInterface();
|
||||||
mediaController->followMediaChanges();
|
mediaController->followMediaChanges();
|
||||||
|
|
||||||
|
connect(monitor, &BluetoothMonitor::deviceConnected, this, &AirPodsTrayApp::bluezDeviceConnected);
|
||||||
|
connect(monitor, &BluetoothMonitor::deviceDisconnected, this, &AirPodsTrayApp::bluezDeviceDisconnected);
|
||||||
|
|
||||||
connect(m_battery, &Battery::primaryChanged, this, &AirPodsTrayApp::primaryChanged);
|
connect(m_battery, &Battery::primaryChanged, this, &AirPodsTrayApp::primaryChanged);
|
||||||
|
|
||||||
CrossDevice.isEnabled = loadCrossDeviceEnabled();
|
CrossDevice.isEnabled = loadCrossDeviceEnabled();
|
||||||
@@ -78,15 +83,6 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QDBusInterface iface("org.bluez", "/org/bluez", "org.bluez.Adapter1");
|
|
||||||
QDBusReply<QVariant> 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();
|
initializeDBus();
|
||||||
initializeBluetooth();
|
initializeBluetooth();
|
||||||
}
|
}
|
||||||
@@ -97,8 +93,6 @@ public:
|
|||||||
delete trayIcon;
|
delete trayIcon;
|
||||||
delete trayMenu;
|
delete trayMenu;
|
||||||
delete discoveryAgent;
|
delete discoveryAgent;
|
||||||
delete bluezInterface;
|
|
||||||
delete mprisInterface;
|
|
||||||
delete socket;
|
delete socket;
|
||||||
delete phoneSocket;
|
delete phoneSocket;
|
||||||
}
|
}
|
||||||
@@ -137,46 +131,7 @@ private:
|
|||||||
bool isEnabled = true; // Ability to disable the feature
|
bool isEnabled = true; // Ability to disable the feature
|
||||||
} CrossDevice;
|
} CrossDevice;
|
||||||
|
|
||||||
void initializeDBus() {
|
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<QVariantMap>(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");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isAirPodsDevice(const QBluetoothDeviceInfo &device)
|
bool isAirPodsDevice(const QBluetoothDeviceInfo &device)
|
||||||
{
|
{
|
||||||
@@ -195,43 +150,11 @@ private:
|
|||||||
LOG_WARN("Phone socket is not open, cannot send notification packet");
|
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) {
|
void disconnectDevice(const QString &devicePath) {
|
||||||
LOG_INFO("Disconnecting device at " << devicePath);
|
LOG_INFO("Disconnecting device at " << devicePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
QDBusInterface *bluezInterface = nullptr;
|
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void connectToDevice(const QString &address) {
|
void connectToDevice(const QString &address) {
|
||||||
LOG_INFO("Connecting to device with address: " << address);
|
LOG_INFO("Connecting to device with address: " << address);
|
||||||
@@ -422,6 +345,11 @@ private slots:
|
|||||||
connectToDevice(device);
|
connectToDevice(device);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
void bluezDeviceConnected(const QString &address)
|
||||||
|
{
|
||||||
|
QBluetoothDeviceInfo device(QBluetoothAddress(address), "", 0);
|
||||||
|
connectToDevice(device);
|
||||||
|
}
|
||||||
|
|
||||||
void onDeviceDisconnected(const QBluetoothAddress &address)
|
void onDeviceDisconnected(const QBluetoothAddress &address)
|
||||||
{
|
{
|
||||||
@@ -431,6 +359,7 @@ private slots:
|
|||||||
LOG_WARN("Socket is still open, closing it");
|
LOG_WARN("Socket is still open, closing it");
|
||||||
socket->close();
|
socket->close();
|
||||||
socket = nullptr;
|
socket = nullptr;
|
||||||
|
discoveryAgent->start();
|
||||||
}
|
}
|
||||||
if (phoneSocket && phoneSocket->isOpen())
|
if (phoneSocket && phoneSocket->isOpen())
|
||||||
{
|
{
|
||||||
@@ -438,6 +367,15 @@ private slots:
|
|||||||
LOG_DEBUG("AIRPODS_DISCONNECTED packet written: " << AirPodsPackets::Connection::AIRPODS_DISCONNECTED.toHex());
|
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)
|
void parseMetadata(const QByteArray &data)
|
||||||
{
|
{
|
||||||
@@ -524,9 +462,6 @@ private slots:
|
|||||||
|
|
||||||
LOG_INFO("Connecting to device: " << device.name());
|
LOG_INFO("Connecting to device: " << device.name());
|
||||||
QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol);
|
QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol);
|
||||||
connect(localSocket, &QBluetoothSocket::disconnected, this, [this, localSocket]() {
|
|
||||||
onDeviceDisconnected(localSocket->peerAddress());
|
|
||||||
});
|
|
||||||
connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() {
|
connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() {
|
||||||
// Start periodic magic pairing attempts
|
// Start periodic magic pairing attempts
|
||||||
QTimer *magicPairingTimer = new QTimer(this);
|
QTimer *magicPairingTimer = new QTimer(this);
|
||||||
@@ -778,26 +713,6 @@ private slots:
|
|||||||
QMetaObject::invokeMethod(this, "handlePhonePacket", Qt::QueuedConnection, Q_ARG(QByteArray, data));
|
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:
|
public:
|
||||||
void handleMediaStateChange(MediaController::MediaState state) {
|
void handleMediaStateChange(MediaController::MediaState state) {
|
||||||
if (state == MediaController::MediaState::Playing) {
|
if (state == MediaController::MediaState::Playing) {
|
||||||
@@ -903,12 +818,12 @@ private:
|
|||||||
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
|
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
|
||||||
QBluetoothSocket *socket = nullptr;
|
QBluetoothSocket *socket = nullptr;
|
||||||
QBluetoothSocket *phoneSocket = nullptr;
|
QBluetoothSocket *phoneSocket = nullptr;
|
||||||
QDBusInterface *mprisInterface;
|
|
||||||
QString connectedDeviceMacAddress;
|
QString connectedDeviceMacAddress;
|
||||||
QByteArray lastBatteryStatus;
|
QByteArray lastBatteryStatus;
|
||||||
QByteArray lastEarDetectionStatus;
|
QByteArray lastEarDetectionStatus;
|
||||||
MediaController* mediaController;
|
MediaController* mediaController;
|
||||||
TrayIconManager *trayManager;
|
TrayIconManager *trayManager;
|
||||||
|
BluetoothMonitor *monitor;
|
||||||
QSettings *settings;
|
QSettings *settings;
|
||||||
|
|
||||||
QString m_batteryStatus;
|
QString m_batteryStatus;
|
||||||
|
|||||||
Reference in New Issue
Block a user