From 4b3e1130e8256dfd823933d2ea2c06ed94c73580 Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Fri, 17 Jan 2025 04:28:58 +0530 Subject: [PATCH] smth works :D --- linux/CMakeLists.txt | 15 +--- linux/Main.qml | 17 +++-- linux/main.cpp | 174 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 176 insertions(+), 30 deletions(-) diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 544ad89..eb764af 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -4,7 +4,7 @@ project(linux VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 6.5 REQUIRED COMPONENTS Quick Widgets Bluetooth) +find_package(Qt6 6.5 REQUIRED COMPONENTS Quick Widgets Bluetooth Multimedia DBus) qt_standard_project_setup(REQUIRES 6.5) @@ -19,19 +19,8 @@ qt_add_qml_module(applinux Main.qml ) -# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. -# If you are developing for iOS or macOS you should consider setting an -# explicit, fixed bundle identifier manually though. -set_target_properties(applinux PROPERTIES -# MACOSX_BUNDLE_GUI_IDENTIFIER com.example.applinux - MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} - MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} - MACOSX_BUNDLE TRUE - WIN32_EXECUTABLE TRUE -) - target_link_libraries(applinux - PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth + PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth Qt6::Multimedia Qt6::DBus ) include(GNUInstallDirs) diff --git a/linux/Main.qml b/linux/Main.qml index 3ace210..41aff4a 100644 --- a/linux/Main.qml +++ b/linux/Main.qml @@ -7,19 +7,24 @@ ApplicationWindow { height: 300 title: "AirPods Settings" property bool ignoreNoiseControlChange: false + property bool isPlaying: false + + Component.onCompleted: { + caToggle.checked = airPodsTrayApp.loadConversationalAwarenessState() + } Column { spacing: 20 padding: 20 Text { - text: "Ear Detection Status: " - id: earDetectionStatus + text: "Battery Status: " + id: batteryStatus } Text { - text: "Battery Status: " - id: batteryStatus + text: "Ear Detection Status: " + id: earDetectionStatus } ComboBox { @@ -33,7 +38,7 @@ ApplicationWindow { } Connections { target: airPodsTrayApp - function onNoiseControlModeChanged(mode) { + onNoiseControlModeChanged: { ignoreNoiseControlChange = true noiseControlMode.currentIndex = mode; ignoreNoiseControlChange = false @@ -44,8 +49,10 @@ ApplicationWindow { Switch { id: caToggle text: "Conversational Awareness" + checked: isPlaying onCheckedChanged: { airPodsTrayApp.setConversationalAwareness(checked) + airPodsTrayApp.saveConversationalAwarenessState(checked) } } } diff --git a/linux/main.cpp b/linux/main.cpp index 91eda0e..fadaa15 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -16,6 +16,14 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include Q_LOGGING_CATEGORY(airpodsApp, "airpodsApp") @@ -65,6 +73,7 @@ public: connect(this, &AirPodsTrayApp::noiseControlModeChanged, this, &AirPodsTrayApp::updateNoiseControlMenu); connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateBatteryTooltip); connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateTrayIcon); + connect(this, &AirPodsTrayApp::earDetectionStatusChanged, this, &AirPodsTrayApp::handleEarDetection); trayIcon->setContextMenu(trayMenu); trayIcon->show(); @@ -77,7 +86,6 @@ public: discoveryAgent->start(); LOG_INFO("AirPodsTrayApp initialized and started device discovery"); - // Check for already connected devices QBluetoothLocalDevice localDevice; connect(&localDevice, &QBluetoothLocalDevice::deviceConnected, this, &AirPodsTrayApp::onDeviceConnected); connect(&localDevice, &QBluetoothLocalDevice::deviceDisconnected, this, &AirPodsTrayApp::onDeviceDisconnected); @@ -90,6 +98,7 @@ public: return; } } + initializeMprisInterface(); } public slots: @@ -119,16 +128,16 @@ public slots: LOG_INFO("Setting noise control mode to: " << mode); QByteArray packet; switch (mode) { - case 0: // Off + case 0: packet = QByteArray::fromHex("0400040009000D01000000"); break; - case 1: // Noise Cancellation + case 1: packet = QByteArray::fromHex("0400040009000D02000000"); break; - case 2: // Transparency + case 2: packet = QByteArray::fromHex("0400040009000D03000000"); break; - case 3: // Adaptive + case 3: packet = QByteArray::fromHex("0400040009000D04000000"); break; } @@ -156,8 +165,19 @@ public slots: for (QAction *action : actions) { action->setChecked(false); } - if (mode >= 0 && mode < actions.size()) { - actions[mode]->setChecked(true); + switch (mode) { + case 0: + actions[0]->setChecked(true); + break; + case 1: + actions[3]->setChecked(true); + break; + case 2: + actions[1]->setChecked(true); + break; + case 3: + actions[2]->setChecked(true); + break; } } @@ -184,11 +204,73 @@ public slots: trayIcon->setIcon(QIcon(pixmap)); } + void handleEarDetection(const QString &status) { + static bool wasPausedByApp = false; + + QStringList parts = status.split(", "); + bool primaryInEar = parts[0].contains("In Ear"); + bool secondaryInEar = parts[1].contains("In Ear"); + + if (primaryInEar && secondaryInEar) { + if (wasPausedByApp) { + QProcess::execute("playerctl", QStringList() << "play"); + LOG_INFO("Resumed playback via Playerctl"); + wasPausedByApp = false; + } + LOG_INFO("Both AirPods are in ear"); + activateA2dpProfile(); + } else { + LOG_INFO("At least one AirPod is out of ear"); + QProcess process; + process.start("playerctl", QStringList() << "status"); + process.waitForFinished(); + QString playbackStatus = process.readAllStandardOutput().trimmed(); + LOG_DEBUG("Playback status: " << playbackStatus); + if (playbackStatus == "Playing") { + QProcess::execute("playerctl", QStringList() << "pause"); + LOG_INFO("Paused playback via Playerctl"); + wasPausedByApp = true; + } + if (!primaryInEar && !secondaryInEar) { + removeAudioOutputDevice(); + } + } + } + + void activateA2dpProfile() { + LOG_INFO("Activating A2DP profile for AirPods"); + QProcess::execute("pactl", QStringList() << "set-card-profile" << "bluez_card." + connectedDeviceMacAddress << "a2dp-sink"); + } + + void removeAudioOutputDevice() { + LOG_INFO("Removing AirPods as audio output device"); + QProcess::execute("pactl", QStringList() << "set-card-profile" << "bluez_card." + connectedDeviceMacAddress << "off"); + } + + bool loadConversationalAwarenessState() { + QFile file(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/ca_state.txt"); + if (file.open(QIODevice::ReadOnly)) { + QTextStream in(&file); + QString state = in.readLine(); + file.close(); + return state == "true"; + } + return false; + } + + void saveConversationalAwarenessState(bool state) { + QFile file(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/ca_state.txt"); + if (file.open(QIODevice::WriteOnly)) { + QTextStream out(&file); + out << (state ? "true" : "false"); + file.close(); + } + } + private slots: void onTrayIconActivated(QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Trigger) { LOG_INFO("Tray icon activated"); - // Show settings window QQuickWindow *window = qobject_cast(QGuiApplication::topLevelWindows().first()); if (window) { window->show(); @@ -245,7 +327,7 @@ private slots: QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol); connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() { LOG_INFO("Connected to device, sending initial packets"); - discoveryAgent->stop(); // Stop discovering once connected + discoveryAgent->stop(); QByteArray handshakePacket = QByteArray::fromHex("00000400010002000000000000000000"); QByteArray setSpecificFeaturesPacket = QByteArray::fromHex("040004004d00ff00000000000000"); @@ -286,6 +368,7 @@ private slots: localSocket->connectToService(device.address(), QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a")); socket = localSocket; + connectedDeviceMacAddress = device.address().toString().replace(":", "_"); } void parseData(const QByteArray &data) { @@ -316,6 +399,73 @@ private slots: .arg(caseLevel); LOG_INFO("Battery status: " << batteryStatus); emit batteryStatusChanged(batteryStatus); + + } else if (data.size() == 10 && data.startsWith(QByteArray::fromHex("040004004B00020001"))) { + LOG_INFO("Received conversational awareness data"); + handleConversationalAwareness(data); + } + } + + void handleConversationalAwareness(const QByteArray &data) { + LOG_DEBUG("Handling conversational awareness data: " << data.toHex()); + static int initialVolume = -1; + bool lowered = data[9] == 0x01; + LOG_INFO("Conversational awareness: " << (lowered ? "enabled" : "disabled")); + + if (lowered) { + if (initialVolume == -1) { + QProcess process; + process.start("pactl", QStringList() << "get-sink-volume" << "@DEFAULT_SINK@"); + process.waitForFinished(); + QString output = process.readAllStandardOutput(); + // Volume: front-left: 12843 / 20% / -42.47 dB, front-right: 12843 / 20% / -42.47 dB + // balance 0.00 + + QRegularExpression re("front-left: \\d+ /\\s*(\\d+)%"); + QRegularExpressionMatch match = re.match(output); + if (match.hasMatch()) { + LOG_DEBUG("Matched: " << match.captured(1)); + initialVolume = match.captured(1).toInt(); + } else { + LOG_ERROR("Failed to parse initial volume from output: " << output); + return; + } + } + QProcess::execute("pactl", QStringList() << "set-sink-volume" << "@DEFAULT_SINK@" << QString::number(initialVolume * 0.20) + "%"); + LOG_INFO("Volume lowered to 0.20 of initial which is " << initialVolume * 0.20 << "%"); + } else { + if (initialVolume != -1) { + QProcess::execute("pactl", QStringList() << "set-sink-volume" << "@DEFAULT_SINK@" << QString::number(initialVolume) + "%"); + LOG_INFO("Volume restored to " << initialVolume << "%"); + initialVolume = -1; + } + } + } + + void initializeMprisInterface() { + QStringList services = QDBusConnection::sessionBus().interface()->registeredServiceNames(); + QString mprisService; + + foreach (const QString &service, services) { + if (service.startsWith("org.mpris.MediaPlayer2.") && service != "org.mpris.MediaPlayer2") { + mprisService = service; + break; + } + } + + if (!mprisService.isEmpty()) { + mprisInterface = new QDBusInterface(mprisService, + "/org/mpris/MediaPlayer2", + "org.mpris.MediaPlayer2.Player", + QDBusConnection::sessionBus(), + this); + if (!mprisInterface->isValid()) { + LOG_ERROR("Failed to initialize MPRIS interface for service: " << mprisService); + } else { + LOG_INFO("Connected to MPRIS service: " << mprisService); + } + } else { + LOG_WARN("No active MPRIS media players found"); } } @@ -329,17 +479,17 @@ private: QMenu *trayMenu; QBluetoothDeviceDiscoveryAgent *discoveryAgent; QBluetoothSocket *socket = nullptr; + QDBusInterface *mprisInterface; + QString connectedDeviceMacAddress; }; int main(int argc, char *argv[]) { QApplication app(argc, argv); QQmlApplicationEngine engine; - engine.loadFromModule("linux", "Main"); - AirPodsTrayApp trayApp; - engine.rootContext()->setContextProperty("airPodsTrayApp", &trayApp); + engine.loadFromModule("linux", "Main"); QObject::connect(&trayApp, &AirPodsTrayApp::noiseControlModeChanged, [&engine](int mode) { LOG_DEBUG("Received noiseControlModeChanged signal with mode: " << mode);