[Linux] New ear detection implementation (#145)

* New ear detection implementation

* [Linux] Improved case battery detection when not connected
This commit is contained in:
Tim Gromeyer
2025-06-07 09:19:14 +02:00
committed by GitHub
parent 3b20540c34
commit 5754dbfb16
9 changed files with 151 additions and 86 deletions

View File

@@ -31,6 +31,7 @@ qt_add_executable(applinux
thirdparty/QR-Code-generator/qrcodegen.cpp thirdparty/QR-Code-generator/qrcodegen.cpp
thirdparty/QR-Code-generator/qrcodegen.hpp thirdparty/QR-Code-generator/qrcodegen.hpp
QRCodeImageProvider.hpp QRCodeImageProvider.hpp
eardetection.hpp
) )
qt_add_qml_module(applinux qt_add_qml_module(applinux

View File

@@ -94,7 +94,7 @@ ApplicationWindow {
spacing: 8 spacing: 8
PodColumn { PodColumn {
isVisible: airPodsTrayApp.deviceInfo.battery.leftPodAvailable visible: airPodsTrayApp.deviceInfo.battery.leftPodAvailable
inEar: airPodsTrayApp.deviceInfo.leftPodInEar inEar: airPodsTrayApp.deviceInfo.leftPodInEar
iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.podIcon iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.podIcon
batteryLevel: airPodsTrayApp.deviceInfo.battery.leftPodLevel batteryLevel: airPodsTrayApp.deviceInfo.battery.leftPodLevel
@@ -103,7 +103,7 @@ ApplicationWindow {
} }
PodColumn { PodColumn {
isVisible: airPodsTrayApp.deviceInfo.battery.rightPodAvailable visible: airPodsTrayApp.deviceInfo.battery.rightPodAvailable
inEar: airPodsTrayApp.deviceInfo.rightPodInEar inEar: airPodsTrayApp.deviceInfo.rightPodInEar
iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.podIcon iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.podIcon
batteryLevel: airPodsTrayApp.deviceInfo.battery.rightPodLevel batteryLevel: airPodsTrayApp.deviceInfo.battery.rightPodLevel
@@ -112,7 +112,7 @@ ApplicationWindow {
} }
PodColumn { PodColumn {
isVisible: airPodsTrayApp.deviceInfo.battery.caseAvailable visible: airPodsTrayApp.deviceInfo.battery.caseAvailable
inEar: true inEar: true
iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.caseIcon iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.caseIcon
batteryLevel: airPodsTrayApp.deviceInfo.battery.caseLevel batteryLevel: airPodsTrayApp.deviceInfo.battery.caseLevel

View File

@@ -1,16 +1,25 @@
import QtQuick 2.15 import QtQuick 2.15
Column { Column {
property bool isVisible: true id: root
property bool inEar: true property bool inEar: true
property string iconSource property string iconSource
property int batteryLevel: 0 property int batteryLevel: 0
property bool isCharging: false property bool isCharging: false
property string indicator: "" property string indicator: ""
property real targetOpacity: inEar ? 1 : 0.5
Timer {
id: opacityTimer
interval: 50
onTriggered: root.opacity = root.targetOpacity
}
onInEarChanged: {
opacityTimer.restart()
}
spacing: 5 spacing: 5
opacity: inEar ? 1 : 0.5
visible: isVisible
Image { Image {
source: parent.iconSource source: parent.iconSource

View File

@@ -7,6 +7,7 @@
#include <climits> #include <climits>
#include "airpods_packets.h" #include "airpods_packets.h"
#include "logger.h"
class Battery : public QObject class Battery : public QObject
{ {
@@ -128,10 +129,14 @@ public:
// Emit signal to notify about battery status change // Emit signal to notify about battery status change
emit batteryStatusChanged(); emit batteryStatusChanged();
// Log which is left and right pod
LOG_INFO("Primary Pod:" << primaryPod);
LOG_INFO("Secondary Pod:" << secondaryPod);
return true; return true;
} }
bool parseEncryptedPacket(const QByteArray &packet, bool isLeftPodPrimary) bool parseEncryptedPacket(const QByteArray &packet, bool isLeftPodPrimary, bool podInCase)
{ {
// Validate packet size (expect 16 bytes based on provided payloads) // Validate packet size (expect 16 bytes based on provided payloads)
if (packet.size() != 16) if (packet.size() != 16)
@@ -171,7 +176,9 @@ public:
// Update states // Update states
states[Component::Left] = {static_cast<quint8>(rawLeftBattery), isLeftCharging ? BatteryStatus::Charging : BatteryStatus::Discharging}; states[Component::Left] = {static_cast<quint8>(rawLeftBattery), isLeftCharging ? BatteryStatus::Charging : BatteryStatus::Discharging};
states[Component::Right] = {static_cast<quint8>(rawRightBattery), isRightCharging ? BatteryStatus::Charging : BatteryStatus::Discharging}; states[Component::Right] = {static_cast<quint8>(rawRightBattery), isRightCharging ? BatteryStatus::Charging : BatteryStatus::Discharging};
if (podInCase) {
states[Component::Case] = {static_cast<quint8>(rawCaseBattery), isCaseCharging ? BatteryStatus::Charging : BatteryStatus::Discharging}; states[Component::Case] = {static_cast<quint8>(rawCaseBattery), isCaseCharging ? BatteryStatus::Charging : BatteryStatus::Discharging};
}
primaryPod = isLeftPodPrimary ? Component::Left : Component::Right; primaryPod = isLeftPodPrimary ? Component::Left : Component::Right;
secondaryPod = isLeftPodPrimary ? Component::Right : Component::Left; secondaryPod = isLeftPodPrimary ? Component::Right : Component::Left;
emit batteryStatusChanged(); emit batteryStatusChanged();

View File

@@ -5,6 +5,7 @@
#include <QSettings> #include <QSettings>
#include "battery.hpp" #include "battery.hpp"
#include "enums.h" #include "enums.h"
#include "eardetection.hpp"
using namespace AirpodsTrayApp::Enums; using namespace AirpodsTrayApp::Enums;
@@ -12,14 +13,11 @@ class DeviceInfo : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QString batteryStatus READ batteryStatus WRITE setBatteryStatus NOTIFY batteryStatusChanged) Q_PROPERTY(QString batteryStatus READ batteryStatus WRITE setBatteryStatus NOTIFY batteryStatusChanged)
Q_PROPERTY(QString earDetectionStatus READ earDetectionStatus WRITE setEarDetectionStatus NOTIFY earDetectionStatusChanged)
Q_PROPERTY(int noiseControlMode READ noiseControlModeInt WRITE setNoiseControlModeInt NOTIFY noiseControlModeChangedInt) Q_PROPERTY(int noiseControlMode READ noiseControlModeInt WRITE setNoiseControlModeInt NOTIFY noiseControlModeChangedInt)
Q_PROPERTY(bool conversationalAwareness READ conversationalAwareness WRITE setConversationalAwareness NOTIFY conversationalAwarenessChanged) Q_PROPERTY(bool conversationalAwareness READ conversationalAwareness WRITE setConversationalAwareness NOTIFY conversationalAwarenessChanged)
Q_PROPERTY(int adaptiveNoiseLevel READ adaptiveNoiseLevel WRITE setAdaptiveNoiseLevel NOTIFY adaptiveNoiseLevelChanged) Q_PROPERTY(int adaptiveNoiseLevel READ adaptiveNoiseLevel WRITE setAdaptiveNoiseLevel NOTIFY adaptiveNoiseLevelChanged)
Q_PROPERTY(QString deviceName READ deviceName WRITE setDeviceName NOTIFY deviceNameChanged) Q_PROPERTY(QString deviceName READ deviceName WRITE setDeviceName NOTIFY deviceNameChanged)
Q_PROPERTY(Battery *battery READ getBattery CONSTANT) Q_PROPERTY(Battery *battery READ getBattery CONSTANT)
Q_PROPERTY(bool primaryInEar READ isPrimaryInEar WRITE setPrimaryInEar NOTIFY primaryChanged)
Q_PROPERTY(bool secondaryInEar READ isSecondaryInEar WRITE setSecondaryInEar NOTIFY primaryChanged)
Q_PROPERTY(bool oneBudANCMode READ oneBudANCMode WRITE setOneBudANCMode NOTIFY oneBudANCModeChanged) Q_PROPERTY(bool oneBudANCMode READ oneBudANCMode WRITE setOneBudANCMode NOTIFY oneBudANCModeChanged)
Q_PROPERTY(AirPodsModel model READ model WRITE setModel NOTIFY modelChanged) Q_PROPERTY(AirPodsModel model READ model WRITE setModel NOTIFY modelChanged)
Q_PROPERTY(bool adaptiveModeActive READ adaptiveModeActive NOTIFY noiseControlModeChangedInt) Q_PROPERTY(bool adaptiveModeActive READ adaptiveModeActive NOTIFY noiseControlModeChangedInt)
@@ -32,7 +30,9 @@ class DeviceInfo : public QObject
Q_PROPERTY(QString magicAccEncKey READ magicAccEncKeyHex CONSTANT) Q_PROPERTY(QString magicAccEncKey READ magicAccEncKeyHex CONSTANT)
public: public:
explicit DeviceInfo(QObject *parent = nullptr) : QObject(parent), m_battery(new Battery(this)) {} explicit DeviceInfo(QObject *parent = nullptr) : QObject(parent), m_battery(new Battery(this)), m_earDetection(new EarDetection(this)) {
connect(getEarDetection(), &EarDetection::statusChanged, this, &DeviceInfo::primaryChanged);
}
QString batteryStatus() const { return m_batteryStatus; } QString batteryStatus() const { return m_batteryStatus; }
void setBatteryStatus(const QString &status) void setBatteryStatus(const QString &status)
@@ -44,16 +44,6 @@ public:
} }
} }
QString earDetectionStatus() const { return m_earDetectionStatus; }
void setEarDetectionStatus(const QString &status)
{
if (m_earDetectionStatus != status)
{
m_earDetectionStatus = status;
emit earDetectionStatusChanged(status);
}
}
NoiseControlMode noiseControlMode() const { return m_noiseControlMode; } NoiseControlMode noiseControlMode() const { return m_noiseControlMode; }
void setNoiseControlMode(NoiseControlMode mode) void setNoiseControlMode(NoiseControlMode mode)
{ {
@@ -99,26 +89,6 @@ public:
Battery *getBattery() const { return m_battery; } Battery *getBattery() const { return m_battery; }
bool isPrimaryInEar() const { return m_primaryInEar; }
void setPrimaryInEar(bool inEar)
{
if (m_primaryInEar != inEar)
{
m_primaryInEar = inEar;
emit primaryChanged();
}
}
bool isSecondaryInEar() const { return m_secoundaryInEar; }
void setSecondaryInEar(bool inEar)
{
if (m_secoundaryInEar != inEar)
{
m_secoundaryInEar = inEar;
emit primaryChanged();
}
}
bool oneBudANCMode() const { return m_oneBudANCMode; } bool oneBudANCMode() const { return m_oneBudANCMode; }
void setOneBudANCMode(bool enabled) void setOneBudANCMode(bool enabled)
{ {
@@ -167,18 +137,18 @@ public:
QString caseIcon() const { return getModelIcon(model()).second; } QString caseIcon() const { return getModelIcon(model()).second; }
bool isLeftPodInEar() const bool isLeftPodInEar() const
{ {
if (getBattery()->getPrimaryPod() == Battery::Component::Left) return isPrimaryInEar(); if (getBattery()->getPrimaryPod() == Battery::Component::Left) return getEarDetection()->isPrimaryInEar();
else return isSecondaryInEar(); else return getEarDetection()->isSecondaryInEar();
} }
bool isRightPodInEar() const bool isRightPodInEar() const
{ {
if (getBattery()->getPrimaryPod() == Battery::Component::Right) return isPrimaryInEar(); if (getBattery()->getPrimaryPod() == Battery::Component::Right) return getEarDetection()->isPrimaryInEar();
else return isSecondaryInEar(); else return getEarDetection()->isSecondaryInEar();
} }
bool adaptiveModeActive() const { return noiseControlMode() == NoiseControlMode::Adaptive; } bool adaptiveModeActive() const { return noiseControlMode() == NoiseControlMode::Adaptive; }
bool oneOrMorePodsInCase() const { return earDetectionStatus().contains("In case"); }
bool oneOrMorePodsInEar() const { return isPrimaryInEar() || isSecondaryInEar(); } EarDetection *getEarDetection() const { return m_earDetection; }
void reset() void reset()
{ {
@@ -186,11 +156,9 @@ public:
setModel(AirPodsModel::Unknown); setModel(AirPodsModel::Unknown);
m_battery->reset(); m_battery->reset();
setBatteryStatus(""); setBatteryStatus("");
setEarDetectionStatus("");
setPrimaryInEar(false);
setSecondaryInEar(false);
setNoiseControlMode(NoiseControlMode::Off); setNoiseControlMode(NoiseControlMode::Off);
setBluetoothAddress(""); setBluetoothAddress("");
getEarDetection()->reset();
} }
void saveToSettings(QSettings &settings) void saveToSettings(QSettings &settings)
@@ -210,9 +178,16 @@ public:
setMagicAccEncKey(settings.value("DeviceInfo/magicAccEncKey", QByteArray()).toByteArray()); setMagicAccEncKey(settings.value("DeviceInfo/magicAccEncKey", QByteArray()).toByteArray());
} }
void updateBatteryStatus()
{
int leftLevel = getBattery()->getState(Battery::Component::Left).level;
int rightLevel = getBattery()->getState(Battery::Component::Right).level;
int caseLevel = getBattery()->getState(Battery::Component::Case).level;
setBatteryStatus(QString("Left: %1%, Right: %2%, Case: %3%").arg(leftLevel).arg(rightLevel).arg(caseLevel));
}
signals: signals:
void batteryStatusChanged(const QString &status); void batteryStatusChanged(const QString &status);
void earDetectionStatusChanged(const QString &status);
void noiseControlModeChanged(NoiseControlMode mode); void noiseControlModeChanged(NoiseControlMode mode);
void noiseControlModeChangedInt(int mode); void noiseControlModeChangedInt(int mode);
void conversationalAwarenessChanged(bool enabled); void conversationalAwarenessChanged(bool enabled);
@@ -225,14 +200,11 @@ signals:
private: private:
QString m_batteryStatus; QString m_batteryStatus;
QString m_earDetectionStatus;
NoiseControlMode m_noiseControlMode = NoiseControlMode::Transparency; NoiseControlMode m_noiseControlMode = NoiseControlMode::Transparency;
bool m_conversationalAwareness = false; bool m_conversationalAwareness = false;
int m_adaptiveNoiseLevel = 50; int m_adaptiveNoiseLevel = 50;
QString m_deviceName; QString m_deviceName;
Battery *m_battery; Battery *m_battery;
bool m_primaryInEar = false;
bool m_secoundaryInEar = false;
QByteArray m_magicAccIRK; QByteArray m_magicAccIRK;
QByteArray m_magicAccEncKey; QByteArray m_magicAccEncKey;
bool m_oneBudANCMode = false; bool m_oneBudANCMode = false;
@@ -240,4 +212,5 @@ private:
QString m_modelNumber; QString m_modelNumber;
QString m_manufacturer; QString m_manufacturer;
QString m_bluetoothAddress; QString m_bluetoothAddress;
EarDetection *m_earDetection;
}; };

94
linux/eardetection.hpp Normal file
View File

@@ -0,0 +1,94 @@
#pragma once
#include <QObject>
#include <QByteArray>
#include <QPair>
#include "logger.h"
class EarDetection : public QObject
{
Q_OBJECT
public:
enum class EarDetectionStatus
{
InEar,
NotInEar,
InCase,
Disconnected,
};
Q_ENUM(EarDetectionStatus)
explicit EarDetection(QObject *parent = nullptr) : QObject(parent)
{
reset();
}
void reset()
{
primaryStatus = EarDetectionStatus::Disconnected;
secondaryStatus = EarDetectionStatus::Disconnected;
emit statusChanged();
}
bool parseData(const QByteArray &data)
{
if (data.size() < 2)
{
return false;
}
auto [newprimaryStatus, newsecondaryStatus] = parseStatusBytes(data);
primaryStatus = newprimaryStatus;
secondaryStatus = newsecondaryStatus;
LOG_DEBUG("Parsed Ear Detection Status: Primary - " << primaryStatus
<< ", Secondary - " << secondaryStatus);
emit statusChanged();
return true;
}
void overrideEarDetectionStatus(bool primaryInEar, bool secondaryInEar)
{
primaryStatus = primaryInEar ? EarDetectionStatus::InEar : EarDetectionStatus::NotInEar;
secondaryStatus = secondaryInEar ? EarDetectionStatus::InEar : EarDetectionStatus::NotInEar;
emit statusChanged();
}
bool isPrimaryInEar() const { return primaryStatus == EarDetectionStatus::InEar; }
bool isSecondaryInEar() const { return secondaryStatus == EarDetectionStatus::InEar; }
bool oneOrMorePodsInCase() const { return primaryStatus == EarDetectionStatus::InCase || secondaryStatus == EarDetectionStatus::InCase; }
bool oneOrMorePodsInEar() const { return isPrimaryInEar() || isSecondaryInEar(); }
EarDetectionStatus getprimaryStatus() const { return primaryStatus; }
EarDetectionStatus getsecondaryStatus() const { return secondaryStatus; }
signals:
void statusChanged();
private:
QPair<EarDetectionStatus, EarDetectionStatus> parseStatusBytes(const QByteArray &data) const
{
quint8 primaryByte = static_cast<quint8>(data[6]);
quint8 secondaryByte = static_cast<quint8>(data[7]);
auto primaryStatus = parseStatusByte(primaryByte);
auto secondaryStatus = parseStatusByte(secondaryByte);
return qMakePair(primaryStatus, secondaryStatus);
}
EarDetectionStatus parseStatusByte(quint8 byte) const
{
if (byte == 0x00)
return EarDetectionStatus::InEar;
if (byte == 0x01)
return EarDetectionStatus::NotInEar;
if (byte == 0x02)
return EarDetectionStatus::InCase;
return EarDetectionStatus::Disconnected;
}
EarDetectionStatus primaryStatus = EarDetectionStatus::Disconnected;
EarDetectionStatus secondaryStatus = EarDetectionStatus::Disconnected;
};

View File

@@ -65,7 +65,6 @@ public:
// Initialize MediaController and connect signals // Initialize MediaController and connect signals
mediaController = new MediaController(this); mediaController = new MediaController(this);
connect(m_deviceInfo, &DeviceInfo::earDetectionStatusChanged, mediaController, &MediaController::handleEarDetection);
connect(mediaController, &MediaController::mediaStateChanged, this, &AirPodsTrayApp::handleMediaStateChange); connect(mediaController, &MediaController::mediaStateChanged, this, &AirPodsTrayApp::handleMediaStateChange);
mediaController->initializeMprisInterface(); mediaController->initializeMprisInterface();
mediaController->followMediaChanges(); mediaController->followMediaChanges();
@@ -590,26 +589,14 @@ private slots:
// Ear Detection // Ear Detection
else if (data.size() == 8 && data.startsWith(AirPodsPackets::Parse::EAR_DETECTION)) else if (data.size() == 8 && data.startsWith(AirPodsPackets::Parse::EAR_DETECTION))
{ {
char primary = data[6]; m_deviceInfo->getEarDetection()->parseData(data);
char secondary = data[7]; mediaController->handleEarDetection(m_deviceInfo->getEarDetection());
m_deviceInfo->setPrimaryInEar(data[6] == 0x00);
m_deviceInfo->setSecondaryInEar(data[7] == 0x00);
m_deviceInfo->setEarDetectionStatus(QString("Primary: %1, Secondary: %2")
.arg(getEarStatus(primary), getEarStatus(secondary)));
LOG_INFO("Ear detection status: " << m_deviceInfo->earDetectionStatus());
} }
// Battery Status // Battery Status
else if (data.size() == 22 && data.startsWith(AirPodsPackets::Parse::BATTERY_STATUS)) else if (data.size() == 22 && data.startsWith(AirPodsPackets::Parse::BATTERY_STATUS))
{ {
m_deviceInfo->getBattery()->parsePacket(data); m_deviceInfo->getBattery()->parsePacket(data);
m_deviceInfo->updateBatteryStatus();
int leftLevel = m_deviceInfo->getBattery()->getState(Battery::Component::Left).level;
int rightLevel = m_deviceInfo->getBattery()->getState(Battery::Component::Right).level;
int caseLevel = m_deviceInfo->getBattery()->getState(Battery::Component::Case).level;
m_deviceInfo->setBatteryStatus(QString("Left: %1%, Right: %2%, Case: %3%")
.arg(leftLevel)
.arg(rightLevel)
.arg(caseLevel));
LOG_INFO("Battery status: " << m_deviceInfo->batteryStatus()); LOG_INFO("Battery status: " << m_deviceInfo->batteryStatus());
} }
// Conversational Awareness Data // Conversational Awareness Data
@@ -623,7 +610,7 @@ private slots:
parseMetadata(data); parseMetadata(data);
initiateMagicPairing(); initiateMagicPairing();
mediaController->setConnectedDeviceMacAddress(m_deviceInfo->bluetoothAddress().replace(":", "_")); mediaController->setConnectedDeviceMacAddress(m_deviceInfo->bluetoothAddress().replace(":", "_"));
if (m_deviceInfo->oneOrMorePodsInEar()) // AirPods get added as output device only after this if (m_deviceInfo->getEarDetection()->oneOrMorePodsInEar()) // AirPods get added as output device only after this
{ {
mediaController->activateA2dpProfile(); mediaController->activateA2dpProfile();
} }
@@ -762,9 +749,8 @@ private slots:
if (BLEUtils::isValidIrkRpa(m_deviceInfo->magicAccIRK(), device.address)) { if (BLEUtils::isValidIrkRpa(m_deviceInfo->magicAccIRK(), device.address)) {
m_deviceInfo->setModel(device.modelName); m_deviceInfo->setModel(device.modelName);
auto decryptet = BLEUtils::decryptLastBytes(device.encryptedPayload, m_deviceInfo->magicAccEncKey()); auto decryptet = BLEUtils::decryptLastBytes(device.encryptedPayload, m_deviceInfo->magicAccEncKey());
m_deviceInfo->getBattery()->parseEncryptedPacket(decryptet, device.primaryLeft); m_deviceInfo->getBattery()->parseEncryptedPacket(decryptet, device.primaryLeft, device.isThisPodInTheCase);
m_deviceInfo->setPrimaryInEar(device.isPrimaryInEar); m_deviceInfo->getEarDetection()->overrideEarDetectionStatus(device.isPrimaryInEar, device.isSecondaryInEar);
m_deviceInfo->setSecondaryInEar(device.isSecondaryInEar);
} }
} }

View File

@@ -1,5 +1,6 @@
#include "mediacontroller.h" #include "mediacontroller.h"
#include "logger.h" #include "logger.h"
#include "eardetection.hpp"
#include <QDebug> #include <QDebug>
#include <QProcess> #include <QProcess>
@@ -38,7 +39,7 @@ void MediaController::initializeMprisInterface() {
} }
} }
void MediaController::handleEarDetection(const QString &status) void MediaController::handleEarDetection(EarDetection *earDetection)
{ {
if (earDetectionBehavior == Disabled) if (earDetectionBehavior == Disabled)
{ {
@@ -46,15 +47,8 @@ void MediaController::handleEarDetection(const QString &status)
return; return;
} }
bool primaryInEar = false; bool primaryInEar = earDetection->isPrimaryInEar();
bool secondaryInEar = false; bool secondaryInEar = earDetection->isSecondaryInEar();
QStringList parts = status.split(", ");
if (parts.size() == 2)
{
primaryInEar = parts[0].contains("In Ear");
secondaryInEar = parts[1].contains("In Ear");
}
LOG_DEBUG("Ear detection status: primaryInEar=" LOG_DEBUG("Ear detection status: primaryInEar="
<< primaryInEar << ", secondaryInEar=" << secondaryInEar << primaryInEar << ", secondaryInEar=" << secondaryInEar

View File

@@ -5,6 +5,7 @@
#include <QObject> #include <QObject>
class QProcess; class QProcess;
class EarDetection;
class MediaController : public QObject class MediaController : public QObject
{ {
@@ -29,7 +30,7 @@ public:
~MediaController(); ~MediaController();
void initializeMprisInterface(); void initializeMprisInterface();
void handleEarDetection(const QString &status); void handleEarDetection(EarDetection*);
void followMediaChanges(); void followMediaChanges();
bool isActiveOutputDeviceAirPods(); bool isActiveOutputDeviceAirPods();
void handleConversationalAwareness(const QByteArray &data); void handleConversationalAwareness(const QByteArray &data);