[Linux] Allow setting ear detection behaviour

This commit is contained in:
Tim Gromeyer
2025-04-18 18:03:43 +02:00
committed by Tim Gromeyer
parent c2db0afdf1
commit 0846c3eb48
3 changed files with 92 additions and 40 deletions

View File

@@ -34,7 +34,8 @@ 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)) { , monitor(new BluetoothMonitor(this))
, m_settings(new QSettings("AirPodsTrayApp", "AirPodsTrayApp")){
if (debugMode) { if (debugMode) {
QLoggingCategory::setFilterRules("airpodsApp.debug=true"); QLoggingCategory::setFilterRules("airpodsApp.debug=true");
} else { } else {
@@ -85,6 +86,7 @@ public:
~AirPodsTrayApp() { ~AirPodsTrayApp() {
saveCrossDeviceEnabled(); saveCrossDeviceEnabled();
saveEarDetectionSettings();
delete trayIcon; delete trayIcon;
delete trayMenu; delete trayMenu;
@@ -265,18 +267,11 @@ public slots:
} }
} }
bool loadCrossDeviceEnabled() bool loadCrossDeviceEnabled() { return m_settings->value("crossdevice/enabled", false).toBool(); }
{ void saveCrossDeviceEnabled() { m_settings->setValue("crossdevice/enabled", CrossDevice.isEnabled); }
QSettings settings;
return settings.value("crossdevice/enabled", false).toBool();
}
void saveCrossDeviceEnabled() int loadEarDetectionSettings() { return m_settings->value("earDetection/setting", MediaController::EarDetectionBehavior::PauseWhenOneRemoved).toInt(); }
{ void saveEarDetectionSettings() { m_settings->setValue("earDetection/setting", mediaController->getEarDetectionBehavior()); }
QSettings settings;
settings.setValue("crossdevice/enabled", CrossDevice.isEnabled);
settings.sync();
}
private slots: private slots:
void onTrayIconActivated() void onTrayIconActivated()
@@ -836,7 +831,7 @@ private:
MediaController* mediaController; MediaController* mediaController;
TrayIconManager *trayManager; TrayIconManager *trayManager;
BluetoothMonitor *monitor; BluetoothMonitor *monitor;
QSettings *settings; QSettings *m_settings;
QString m_batteryStatus; QString m_batteryStatus;
QString m_earDetectionStatus; QString m_earDetectionStatus;

View File

@@ -38,12 +38,20 @@ void MediaController::initializeMprisInterface() {
} }
} }
void MediaController::handleEarDetection(const QString &status) { void MediaController::handleEarDetection(const QString &status)
{
if (earDetectionBehavior == Disabled)
{
LOG_DEBUG("Ear detection is disabled, ignoring status");
return;
}
bool primaryInEar = false; bool primaryInEar = false;
bool secondaryInEar = false; bool secondaryInEar = false;
QStringList parts = status.split(", "); QStringList parts = status.split(", ");
if (parts.size() == 2) { if (parts.size() == 2)
{
primaryInEar = parts[0].contains("In Ear"); primaryInEar = parts[0].contains("In Ear");
secondaryInEar = parts[1].contains("In Ear"); secondaryInEar = parts[1].contains("In Ear");
} }
@@ -51,37 +59,68 @@ void MediaController::handleEarDetection(const QString &status) {
LOG_DEBUG("Ear detection status: primaryInEar=" LOG_DEBUG("Ear detection status: primaryInEar="
<< primaryInEar << ", secondaryInEar=" << secondaryInEar << primaryInEar << ", secondaryInEar=" << secondaryInEar
<< ", isAirPodsActive=" << isActiveOutputDeviceAirPods()); << ", isAirPodsActive=" << isActiveOutputDeviceAirPods());
if (primaryInEar || secondaryInEar) {
LOG_INFO("At least one AirPod is in ear"); // First handle playback pausing based on selected behavior
activateA2dpProfile(); bool shouldPause = false;
} else { bool shouldResume = false;
LOG_INFO("Both AirPods are out of ear");
removeAudioOutputDevice(); if (earDetectionBehavior == PauseWhenOneRemoved)
{
shouldPause = !primaryInEar || !secondaryInEar;
shouldResume = primaryInEar && secondaryInEar;
}
else if (earDetectionBehavior == PauseWhenBothRemoved)
{
shouldPause = !primaryInEar && !secondaryInEar;
shouldResume = primaryInEar || secondaryInEar;
} }
if (primaryInEar && secondaryInEar) { if (shouldPause && isActiveOutputDeviceAirPods())
if (wasPausedByApp && isActiveOutputDeviceAirPods()) { {
QProcess process;
process.start("playerctl", QStringList() << "status");
process.waitForFinished();
QString playbackStatus = process.readAllStandardOutput().trimmed();
LOG_DEBUG("Playback status: " << playbackStatus);
if (playbackStatus == "Playing")
{
pause();
}
}
// Then handle device profile switching
if (primaryInEar || secondaryInEar)
{
LOG_INFO("At least one AirPod is in ear");
activateA2dpProfile();
// Resume if conditions are met and we previously paused
if (shouldResume && wasPausedByApp && isActiveOutputDeviceAirPods())
{
int result = QProcess::execute("playerctl", QStringList() << "play"); int result = QProcess::execute("playerctl", QStringList() << "play");
LOG_DEBUG("Executed 'playerctl play' with result: " << result); LOG_DEBUG("Executed 'playerctl play' with result: " << result);
if (result == 0) { if (result == 0)
{
LOG_INFO("Resumed playback via Playerctl"); LOG_INFO("Resumed playback via Playerctl");
wasPausedByApp = false; wasPausedByApp = false;
} else { }
else
{
LOG_ERROR("Failed to resume playback via Playerctl"); LOG_ERROR("Failed to resume playback via Playerctl");
} }
} }
} else {
if (isActiveOutputDeviceAirPods()) {
QProcess process;
process.start("playerctl", QStringList() << "status");
process.waitForFinished();
QString playbackStatus = process.readAllStandardOutput().trimmed();
LOG_DEBUG("Playback status: " << playbackStatus);
if (playbackStatus == "Playing") {
pause();
}
}
} }
else
{
LOG_INFO("Both AirPods are out of ear");
removeAudioOutputDevice();
}
}
void MediaController::setEarDetectionBehavior(EarDetectionBehavior behavior)
{
earDetectionBehavior = behavior;
LOG_INFO("Set ear detection behavior to: " << behavior);
} }
void MediaController::followMediaChanges() { void MediaController::followMediaChanges() {

View File

@@ -6,14 +6,28 @@
class QProcess; class QProcess;
class MediaController : public QObject { class MediaController : public QObject
{
Q_OBJECT Q_OBJECT
public: public:
enum MediaState { Playing, Paused, Stopped }; enum MediaState
{
Playing,
Paused,
Stopped
};
Q_ENUM(MediaState)
enum EarDetectionBehavior
{
PauseWhenOneRemoved,
PauseWhenBothRemoved,
Disabled
};
Q_ENUM(EarDetectionBehavior)
explicit MediaController(QObject *parent = nullptr); explicit MediaController(QObject *parent = nullptr);
~MediaController(); ~MediaController();
void initializeMprisInterface(); void initializeMprisInterface();
void handleEarDetection(const QString &status); void handleEarDetection(const QString &status);
void followMediaChanges(); void followMediaChanges();
@@ -23,6 +37,9 @@ public:
void removeAudioOutputDevice(); void removeAudioOutputDevice();
void setConnectedDeviceMacAddress(const QString &macAddress); void setConnectedDeviceMacAddress(const QString &macAddress);
void setEarDetectionBehavior(EarDetectionBehavior behavior);
inline EarDetectionBehavior getEarDetectionBehavior() const { return earDetectionBehavior; }
void pause(); void pause();
Q_SIGNALS: Q_SIGNALS:
@@ -36,6 +53,7 @@ private:
bool wasPausedByApp = false; bool wasPausedByApp = false;
int initialVolume = -1; int initialVolume = -1;
QString connectedDeviceMacAddress; QString connectedDeviceMacAddress;
EarDetectionBehavior earDetectionBehavior = PauseWhenOneRemoved;
}; };
#endif // MEDIACONTROLLER_H #endif // MEDIACONTROLLER_H