Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 153 additions & 3 deletions linux/BluetoothMonitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
#include <QDebug>
#include <QDBusObjectPath>
#include <QDBusMetaType>
#include <QProcess>
#include <QDir>
#include <QDirIterator>
#include <QRegularExpression>
#include <QFile>

BluetoothMonitor::BluetoothMonitor(QObject *parent)
: QObject(parent), m_dbus(QDBusConnection::systemBus())
Expand Down Expand Up @@ -52,15 +57,160 @@ bool BluetoothMonitor::isAirPodsDevice(const QString &devicePath)
return uuids.contains("74ec2172-0bad-4d01-8f77-997b2be0722a");
}

QString BluetoothMonitor::getDeviceName(const QString &devicePath)
QString BluetoothMonitor::getDevicePath(const QString &macAddress)
{
// Convert MAC address to lowercase and replace colons with underscores
QString formattedMac = macAddress.toLower().replace(":", "_");

// List all BlueZ devices
QDBusInterface objectManager("org.bluez", "/", "org.freedesktop.DBus.ObjectManager", m_dbus);
QDBusMessage reply = objectManager.call("GetManagedObjects");

if (reply.type() == QDBusMessage::ReplyMessage) {
ManagedObjectList objects = qdbus_cast<ManagedObjectList>(reply.arguments().at(0));

// Look for the device with matching address
for (const auto &path : objects.keys()) {
QString objPath = path.path();
if (objPath.contains(formattedMac)) {
return objPath;
}
}
}

// If no matching device is found, try the default path
return QString("/org/bluez/hci0/dev_%1").arg(formattedMac);
}

QStringList BluetoothMonitor::findAdapters()
{
QStringList adapters;
QDBusInterface manager("org.bluez", "/", "org.freedesktop.DBus.ObjectManager", m_dbus);
QDBusMessage reply = manager.call("GetManagedObjects");

if (reply.type() == QDBusMessage::ReplyMessage) {
ManagedObjectList objects = qdbus_cast<ManagedObjectList>(reply.arguments().at(0));
for (const auto &path : objects.keys()) {
QString objPath = path.path();
if (objPath.contains("/org/bluez/hci")) {
adapters.append(objPath);
}
}
}
return adapters;
}

QString BluetoothMonitor::getDeviceNameFromBluetooth(const QString &macAddress)
{
// Try all available adapters
QStringList adapters = findAdapters();
for (const QString &adapter : adapters) {
QDBusInterface adapterInterface("org.bluez", adapter, "org.bluez.Adapter1", m_dbus);
QDBusReply<QDBusObjectPath> deviceReply = adapterInterface.call("GetDevice", macAddress);

if (deviceReply.isValid()) {
QDBusInterface deviceInterface("org.bluez", deviceReply.value().path(),
"org.freedesktop.DBus.Properties", m_dbus);
QDBusReply<QVariant> nameReply = deviceInterface.call("Get", "org.bluez.Device1", "Name");

if (nameReply.isValid() && !nameReply.value().toString().isEmpty()) {
return nameReply.value().toString();
}
}
}
return QString();
}

QString BluetoothMonitor::getDeviceNameFromBluetoothctl(const QString &macAddress)
{
QProcess process;
process.start("bluetoothctl", QStringList() << "info" << macAddress);
process.waitForFinished(2000);

if (process.exitCode() == 0) {
QString output = QString::fromUtf8(process.readAllStandardOutput());
QRegularExpression nameRegex("Name:\\s*(.+)\\n");
QRegularExpressionMatch match = nameRegex.match(output);

if (match.hasMatch()) {
return match.captured(1).trimmed();
}
}
return QString();
}

QString BluetoothMonitor::getDeviceNameFromCache(const QString &macAddress)
{
// Check common cache locations
QStringList cachePaths = {
"/var/lib/bluetooth",
QDir::homePath() + "/.cache/bluetooth"
};

QString formattedMac = macAddress.toLower();
for (const QString &basePath : cachePaths) {
QDirIterator it(basePath, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
while (it.hasNext()) {
QString path = it.next();
if (path.contains(formattedMac)) {
QFile infoFile(path + "/info");
if (infoFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString content = QString::fromUtf8(infoFile.readAll());
QRegularExpression nameRegex("Name=(.+)\\n");
QRegularExpressionMatch match = nameRegex.match(content);
if (match.hasMatch()) {
return match.captured(1).trimmed();
}
}
}
}
}
return QString();
}

QString BluetoothMonitor::getDeviceName(const QString &macAddress)
{
LOG_INFO("Attempting to resolve name for device: " << macAddress);

// First try the standard BlueZ D-Bus interface
QString devicePath = getDevicePath(macAddress);
LOG_INFO("Trying BlueZ D-Bus interface with path: " << devicePath);
QDBusInterface deviceInterface("org.bluez", devicePath, "org.freedesktop.DBus.Properties", m_dbus);
QDBusReply<QVariant> nameReply = deviceInterface.call("Get", "org.bluez.Device1", "Name");
if (nameReply.isValid())

if (nameReply.isValid() && !nameReply.value().toString().isEmpty())
{
LOG_INFO("Found name via BlueZ D-Bus: " << nameReply.value().toString());
return nameReply.value().toString();
}
return "Unknown";

// Try alternative BlueZ method
LOG_INFO("Trying alternative BlueZ method...");
QString name = getDeviceNameFromBluetooth(macAddress);
if (!name.isEmpty()) {
LOG_INFO("Found name via alternative BlueZ method: " << name);
return name;
}

// Try bluetoothctl command
LOG_INFO("Trying bluetoothctl command...");
name = getDeviceNameFromBluetoothctl(macAddress);
if (!name.isEmpty()) {
LOG_INFO("Found name via bluetoothctl: " << name);
return name;
}

// Try reading from cache
LOG_INFO("Trying to read from cache...");
name = getDeviceNameFromCache(macAddress);
if (!name.isEmpty()) {
LOG_INFO("Found name in cache: " << name);
return name;
}

// If all methods fail, return the MAC address
LOG_WARN("Could not resolve device name for MAC: " << macAddress);
return macAddress;
}

bool BluetoothMonitor::checkAlreadyConnectedDevices()
Expand Down
9 changes: 8 additions & 1 deletion linux/BluetoothMonitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ private slots:
QDBusConnection m_dbus;
void registerDBusService();
bool isAirPodsDevice(const QString &devicePath);
QString getDeviceName(const QString &devicePath);
QString getDevicePath(const QString &macAddress);
QString getDeviceNameFromBluetooth(const QString &macAddress);
QString getDeviceNameFromBluetoothctl(const QString &macAddress);
QString getDeviceNameFromCache(const QString &macAddress);
QStringList findAdapters();

public:
QString getDeviceName(const QString &macAddress);
};

#endif // BLUETOOTHMONITOR_H
2 changes: 1 addition & 1 deletion linux/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ ApplicationWindow {

Switch {
visible: airPodsTrayApp.airpodsConnected
text: "Conversational Awareness"
text: "Conversational Awareness" + (airPodsTrayApp.connectedPhoneName ? " (" + airPodsTrayApp.connectedPhoneName + ")" : "")
checked: airPodsTrayApp.deviceInfo.conversationalAwareness
// Disable when no phone MAC set or using default placeholder
enabled: !(PHONE_MAC_ADDRESS === "" || PHONE_MAC_ADDRESS === "00:00:00:00:00:00")
Expand Down
9 changes: 9 additions & 0 deletions linux/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ class AirPodsTrayApp : public QObject {
Q_PROPERTY(bool hideOnStart READ hideOnStart CONSTANT)
Q_PROPERTY(DeviceInfo *deviceInfo READ deviceInfo CONSTANT)
Q_PROPERTY(QString phoneMacStatus READ phoneMacStatus NOTIFY phoneMacStatusChanged)
Q_PROPERTY(QString connectedPhoneName READ connectedPhoneName NOTIFY connectedPhoneNameChanged)

public:
QString connectedPhoneName() const { return m_connectedPhoneName; }
AirPodsTrayApp(bool debugMode, bool hideOnStart, QQmlApplicationEngine *parent = nullptr)
: QObject(parent), debugMode(debugMode), m_settings(new QSettings("AirPodsTrayApp", "AirPodsTrayApp"))
, m_autoStartManager(new AutoStartManager(this)), m_hideOnStart(hideOnStart), parent(parent)
Expand Down Expand Up @@ -126,6 +128,7 @@ class AirPodsTrayApp : public QObject {
private:
bool debugMode;
bool isConnectedLocally = false;
QString m_connectedPhoneName;

QQmlApplicationEngine *parent = nullptr;

Expand Down Expand Up @@ -710,6 +713,11 @@ private slots:
if (!env.value("PHONE_MAC_ADDRESS").isEmpty())
{
phoneAddress = QBluetoothAddress(env.value("PHONE_MAC_ADDRESS"));
// Get the phone name using BluetoothMonitor
BluetoothMonitor monitor;
QString phoneName = monitor.getDeviceName(phoneAddress.toString());
m_connectedPhoneName = phoneName.isEmpty() ? phoneAddress.toString() : phoneName;
emit connectedPhoneNameChanged();
}
phoneSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol);
connect(phoneSocket, &QBluetoothSocket::connected, this, [this]() {
Expand Down Expand Up @@ -895,6 +903,7 @@ private slots:
void conversationalAwarenessChanged(bool enabled);
void adaptiveNoiseLevelChanged(int level);
void deviceNameChanged(const QString &name);
void connectedPhoneNameChanged();
void modelChanged();
void primaryChanged();
void airPodsStatusChanged();
Expand Down
11 changes: 11 additions & 0 deletions linux/trayiconmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ void TrayIconManager::updateConversationalAwareness(bool enabled)
caToggleAction->setChecked(enabled);
}

void TrayIconManager::setPhoneName(const QString &name)
{
m_phoneName = name;
// Update the action text to include the phone name when available
if (m_phoneName.isEmpty()) {
caToggleAction->setText("Toggle Conversational Awareness");
} else {
caToggleAction->setText(QString("Toggle Conversational Awareness — %1").arg(m_phoneName));
}
}

void TrayIconManager::setupMenuActions()
{
// Open action
Expand Down
4 changes: 4 additions & 0 deletions linux/trayiconmanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ class TrayIconManager : public QObject
}
}

// Set or clear the resolved phone name displayed next to the Conversational Awareness menu entry
void setPhoneName(const QString &name);

void resetTrayIcon()
{
trayIcon->setIcon(QIcon(":/icons/assets/airpods.png"));
Expand All @@ -51,6 +54,7 @@ private slots:
QAction *caToggleAction;
QActionGroup *noiseControlGroup;
bool m_notificationsEnabled = true;
QString m_phoneName;

void setupMenuActions();

Expand Down