diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 2ac0f6f..94555c7 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 Multimedia DBus) +find_package(Qt6 6.5 REQUIRED COMPONENTS Quick Widgets Bluetooth DBus) qt_standard_project_setup(REQUIRES 6.5) @@ -15,6 +15,9 @@ qt_add_executable(applinux mediacontroller.cpp mediacontroller.h airpods_packets.h + trayiconmanager.cpp + trayiconmanager.h + enums.h ) qt_add_qml_module(applinux @@ -24,8 +27,15 @@ qt_add_qml_module(applinux Main.qml ) +# Add the resource file +qt_add_resources(applinux "resources" + PREFIX "/icons" + FILES + assets/airpods.png +) + target_link_libraries(applinux - PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth Qt6::Multimedia Qt6::DBus + PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth Qt6::DBus ) include(GNUInstallDirs) diff --git a/linux/Main.qml b/linux/Main.qml index f5863b4..d980272 100644 --- a/linux/Main.qml +++ b/linux/Main.qml @@ -6,58 +6,35 @@ ApplicationWindow { width: 400 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: "Battery Status: " id: batteryStatus - objectName: "batteryStatus" + text: "Battery Status: " + airPodsTrayApp.batteryStatus color: "#ffffff" } Text { - text: "Ear Detection Status: " id: earDetectionStatus - objectName: "earDetectionStatus" + text: "Ear Detection Status: " + airPodsTrayApp.earDetectionStatus color: "#ffffff" } ComboBox { id: noiseControlMode model: ["Off", "Noise Cancellation", "Transparency", "Adaptive"] - currentIndex: 0 - onCurrentIndexChanged: { - if (!ignoreNoiseControlChange) { - airPodsTrayApp.setNoiseControlMode(currentIndex) - } - } - Connections { - target: airPodsTrayApp - onNoiseControlModeChanged: { - ignoreNoiseControlChange = true - noiseControlMode.currentIndex = mode; - ignoreNoiseControlChange = false - } - } + currentIndex: airPodsTrayApp.noiseControlMode + onCurrentIndexChanged: airPodsTrayApp.noiseControlMode = currentIndex } Switch { id: caToggle text: "Conversational Awareness" - checked: isPlaying - onCheckedChanged: { - airPodsTrayApp.setConversationalAwareness(checked) - airPodsTrayApp.saveConversationalAwarenessState(checked) - } + checked: airPodsTrayApp.conversationalAwareness + onCheckedChanged: airPodsTrayApp.conversationalAwareness = checked } } -} +} \ No newline at end of file diff --git a/linux/assets/airpods.png b/linux/assets/airpods.png new file mode 100644 index 0000000..861ddf7 Binary files /dev/null and b/linux/assets/airpods.png differ diff --git a/linux/enums.h b/linux/enums.h new file mode 100644 index 0000000..9a94d96 --- /dev/null +++ b/linux/enums.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace AirpodsTrayApp +{ + namespace Enums + { + Q_NAMESPACE + + enum class NoiseControlMode : quint8 + { + Off = 0, + NoiseCancellation = 1, + Transparency = 2, + Adaptive = 3, + + MinValue = Off, + MaxValue = Adaptive, + }; + Q_ENUM_NS(NoiseControlMode) + } +} \ No newline at end of file diff --git a/linux/main.cpp b/linux/main.cpp index f3bbee4..74ebb1c 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -1,26 +1,24 @@ +#include + #include "main.h" #include "airpods_packets.h" #include "logger.h" #include "mediacontroller.h" +#include "trayiconmanager.h" +#include "enums.h" + +using namespace AirpodsTrayApp::Enums; Q_LOGGING_CATEGORY(airpodsApp, "airpodsApp") class AirPodsTrayApp : public QObject { Q_OBJECT + Q_PROPERTY(QString batteryStatus READ batteryStatus NOTIFY batteryStatusChanged) + Q_PROPERTY(QString earDetectionStatus READ earDetectionStatus NOTIFY earDetectionStatusChanged) + Q_PROPERTY(int noiseControlMode READ noiseControlMode WRITE setNoiseControlMode NOTIFY noiseControlModeChanged) + Q_PROPERTY(bool conversationalAwareness READ conversationalAwareness WRITE setConversationalAwareness NOTIFY conversationalAwarenessChanged) public: - enum NoiseControlMode : quint8 - { - Off = 0, - NoiseCancellation = 1, - Transparency = 2, - Adaptive = 3, - - MinValue = Off, - MaxValue = Adaptive, - }; - Q_ENUM(NoiseControlMode) - AirPodsTrayApp(bool debugMode) : debugMode(debugMode) { if (debugMode) { QLoggingCategory::setFilterRules("airpodsApp.debug=true"); @@ -28,64 +26,15 @@ public: QLoggingCategory::setFilterRules("airpodsApp.debug=false"); } LOG_INFO("Initializing AirPodsTrayApp"); - trayIcon = new QSystemTrayIcon(QIcon(":/icons/airpods.png")); - trayMenu = new QMenu(); - bool caState = loadConversationalAwarenessState(); - QAction *caToggleAction = new QAction("Toggle Conversational Awareness", trayMenu); - caToggleAction->setCheckable(true); - caToggleAction->setChecked(caState); - connect(caToggleAction, &QAction::triggered, this, [this, caToggleAction]() { - bool newState = !caToggleAction->isChecked(); - setConversationalAwareness(newState); - saveConversationalAwarenessState(newState); - caToggleAction->setChecked(newState); - }); - trayMenu->addAction(caToggleAction); - - QAction *offAction = new QAction("Off", trayMenu); - QAction *transparencyAction = new QAction("Transparency", trayMenu); - QAction *adaptiveAction = new QAction("Adaptive", trayMenu); - QAction *noiseCancellationAction = new QAction("Noise Cancellation", trayMenu); - - offAction->setData(NoiseControlMode::Off); - transparencyAction->setData(NoiseControlMode::Transparency); - adaptiveAction->setData(NoiseControlMode::Adaptive); - noiseCancellationAction->setData(NoiseControlMode::NoiseCancellation); - - offAction->setCheckable(true); - transparencyAction->setCheckable(true); - adaptiveAction->setCheckable(true); - noiseCancellationAction->setCheckable(true); - - trayMenu->addAction(offAction); - trayMenu->addAction(transparencyAction); - trayMenu->addAction(adaptiveAction); - trayMenu->addAction(noiseCancellationAction); - - QActionGroup *noiseControlGroup = new QActionGroup(trayMenu); - noiseControlGroup->addAction(offAction); - noiseControlGroup->addAction(transparencyAction); - noiseControlGroup->addAction(adaptiveAction); - noiseControlGroup->addAction(noiseCancellationAction); - - connect(offAction, &QAction::triggered, this, [this]() - { setNoiseControlMode(NoiseControlMode::Off); }); - connect(transparencyAction, &QAction::triggered, this, [this]() - { setNoiseControlMode(NoiseControlMode::Transparency); }); - connect(adaptiveAction, &QAction::triggered, this, [this]() - { setNoiseControlMode(NoiseControlMode::Adaptive); }); - connect(noiseCancellationAction, &QAction::triggered, this, [this]() - { setNoiseControlMode(NoiseControlMode::NoiseCancellation); }); - - connect(this, &AirPodsTrayApp::noiseControlModeChanged, this, &AirPodsTrayApp::updateNoiseControlMenu); - connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateBatteryTooltip); - connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateTrayIcon); - - trayIcon->setContextMenu(trayMenu); - trayIcon->show(); - - connect(trayIcon, &QSystemTrayIcon::activated, this, &AirPodsTrayApp::onTrayIconActivated); + // Initialize tray icon and connect signals + trayManager = new TrayIconManager(this); + connect(trayManager, &TrayIconManager::trayClicked, this, &AirPodsTrayApp::onTrayIconActivated); + connect(trayManager, &TrayIconManager::noiseControlChanged, this, qOverload(&AirPodsTrayApp::setNoiseControlMode)); + connect(trayManager, &TrayIconManager::conversationalAwarenessToggled, this, &AirPodsTrayApp::setConversationalAwareness); + connect(this, &AirPodsTrayApp::batteryStatusChanged, trayManager, &TrayIconManager::updateBatteryStatus); + connect(this, &AirPodsTrayApp::noiseControlModeChanged, trayManager, &TrayIconManager::updateNoiseControlState); + connect(this, &AirPodsTrayApp::conversationalAwarenessChanged, trayManager, &TrayIconManager::updateConversationalAwareness); // Initialize MediaController and connect signals mediaController = new MediaController(this); @@ -94,6 +43,9 @@ public: mediaController->initializeMprisInterface(); mediaController->followMediaChanges(); + // load conversational awareness state + setConversationalAwareness(loadConversationalAwarenessState()); + discoveryAgent = new QBluetoothDeviceDiscoveryAgent(); discoveryAgent->setLowEnergyDiscoveryTimeout(15000); @@ -139,6 +91,11 @@ public: delete phoneSocket; } + QString batteryStatus() const { return m_batteryStatus; } + QString earDetectionStatus() const { return m_earDetectionStatus; } + int noiseControlMode() const { return static_cast(m_noiseControlMode); } + bool conversationalAwareness() const { return m_conversationalAwareness; } + private: bool debugMode; bool isConnectedLocally = false; @@ -265,120 +222,82 @@ public slots: QByteArray packet; switch (mode) { - case Off: + case NoiseControlMode::Off: packet = AirPodsPackets::NoiseControl::OFF; break; - case NoiseCancellation: + case NoiseControlMode::NoiseCancellation: packet = AirPodsPackets::NoiseControl::NOISE_CANCELLATION; break; - case Transparency: + case NoiseControlMode::Transparency: packet = AirPodsPackets::NoiseControl::TRANSPARENCY; break; - case Adaptive: + case NoiseControlMode::Adaptive: packet = AirPodsPackets::NoiseControl::ADAPTIVE; break; } - if (socket && socket->isOpen()) - { - socket->write(packet); - LOG_DEBUG("Noise control mode packet written: " << packet.toHex()); - } - else - { - LOG_ERROR("Socket is not open, cannot write noise control mode packet"); - } + writePacketToSocket(packet, "Noise control mode packet written: "); + } + void setNoiseControlMode(int mode) + { + setNoiseControlMode(static_cast(mode)); } void setConversationalAwareness(bool enabled) { + if (m_conversationalAwareness == enabled) + { + LOG_INFO("Conversational awareness is already " << (enabled ? "enabled" : "disabled")); + return; + } + LOG_INFO("Setting conversational awareness to: " << (enabled ? "enabled" : "disabled")); QByteArray packet = enabled ? AirPodsPackets::ConversationalAwareness::ENABLED : AirPodsPackets::ConversationalAwareness::DISABLED; + + writePacketToSocket(packet, "Conversational awareness packet written: "); + m_conversationalAwareness = enabled; + emit conversationalAwarenessChanged(enabled); + saveConversationalAwarenessState(); + } + + bool writePacketToSocket(const QByteArray &packet, const QString &logMessage) + { if (socket && socket->isOpen()) { socket->write(packet); - LOG_DEBUG("Conversational awareness packet written: " << packet.toHex()); + LOG_DEBUG(logMessage << packet.toHex()); + return true; } else { - LOG_ERROR("Socket is not open, cannot write conversational awareness packet"); + LOG_ERROR("Socket is not open, cannot write packet"); + return false; } } - void updateNoiseControlMenu(NoiseControlMode mode) { - QList actions = trayMenu->actions(); - for (QAction *action : actions) { - action->setChecked(action->data().toInt() == mode); - } + bool loadConversationalAwarenessState() + { + QSettings settings; + return settings.value("conversationalAwareness", false).toBool(); } - void updateBatteryTooltip(const QString &status) { - trayIcon->setToolTip("Battery Status: " + status); + void saveConversationalAwarenessState() + { + QSettings settings; + settings.setValue("conversationalAwareness", m_conversationalAwareness); + settings.sync(); } - void updateTrayIcon(const QString &status) { - QStringList parts = status.split(", "); - int leftLevel = parts[0].split(": ")[1].replace("%", "").toInt(); - int rightLevel = parts[1].split(": ")[1].replace("%", "").toInt(); - - int minLevel; - if (leftLevel == 0) +private slots: + void onTrayIconActivated() + { + QQuickWindow *window = qobject_cast( + QGuiApplication::topLevelWindows().constFirst()); + if (window) { - minLevel = rightLevel; - } - else if (rightLevel == 0) - { - minLevel = leftLevel; - } - else - { - minLevel = qMin(leftLevel, rightLevel); - } - - - QPixmap pixmap(32, 32); - pixmap.fill(Qt::transparent); - - QPainter painter(&pixmap); - QColor textColor = QApplication::palette().color(QPalette::WindowText); - painter.setPen(textColor); - painter.setFont(QFont("Arial", 12, QFont::Bold)); - painter.drawText(pixmap.rect(), Qt::AlignCenter, QString::number(minLevel) + "%"); - painter.end(); - - trayIcon->setIcon(QIcon(pixmap)); - } - - 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"); - QQuickWindow *window = qobject_cast( - QGuiApplication::topLevelWindows().constFirst()); - if (window) { - window->show(); - window->raise(); - window->requestActivate(); - } + window->show(); + window->raise(); + window->requestActivate(); } } @@ -516,11 +435,11 @@ public slots: if (data.size() == 11 && data.startsWith(AirPodsPackets::NoiseControl::HEADER)) { quint8 rawMode = data[7] - 1; // Offset still needed due to protocol - if (rawMode >= NoiseControlMode::MinValue && rawMode <= NoiseControlMode::MaxValue) + if (rawMode >= (int)NoiseControlMode::MinValue && rawMode <= (int)NoiseControlMode::MaxValue) { - NoiseControlMode mode = static_cast(rawMode); + m_noiseControlMode = static_cast(rawMode); LOG_INFO("Noise control mode: " << rawMode); - emit noiseControlModeChanged(mode); + emit noiseControlModeChanged(m_noiseControlMode); } else { @@ -532,10 +451,10 @@ public slots: { char primary = data[6]; char secondary = data[7]; - QString earDetectionStatus = QString("Primary: %1, Secondary: %2") - .arg(getEarStatus(primary), getEarStatus(secondary)); - LOG_INFO("Ear detection status: " << earDetectionStatus); - emit earDetectionStatusChanged(earDetectionStatus); + m_earDetectionStatus = QString("Primary: %1, Secondary: %2") + .arg(getEarStatus(primary), getEarStatus(secondary)); + LOG_INFO("Ear detection status: " << m_earDetectionStatus); + emit earDetectionStatusChanged(m_earDetectionStatus); } // Battery Status else if (data.size() == 22 && data.startsWith(AirPodsPackets::Parse::BATTERY_STATUS)) @@ -543,12 +462,12 @@ public slots: int leftLevel = data[9]; int rightLevel = data[14]; int caseLevel = data[19]; - QString batteryStatus = QString("Left: %1%, Right: %2%, Case: %3%") - .arg(leftLevel) - .arg(rightLevel) - .arg(caseLevel); - LOG_INFO("Battery status: " << batteryStatus); - emit batteryStatusChanged(batteryStatus); + m_batteryStatus = QString("Left: %1%, Right: %2%, Case: %3%") + .arg(leftLevel) + .arg(rightLevel) + .arg(caseLevel); + LOG_INFO("Battery status: " << m_batteryStatus); + emit batteryStatusChanged(m_batteryStatus); } // Conversational Awareness Data else if (data.size() == 10 && data.startsWith(AirPodsPackets::ConversationalAwareness::DATA_HEADER)) @@ -712,6 +631,11 @@ public slots: } void connectToAirPods(bool force) { + if (socket && socket->isOpen()) { + LOG_INFO("Already connected to AirPods"); + return; + } + if (force) { LOG_INFO("Forcing connection to AirPods"); QProcess process; @@ -773,6 +697,7 @@ signals: void noiseControlModeChanged(NoiseControlMode mode); void earDetectionStatusChanged(const QString &status); void batteryStatusChanged(const QString &status); + void conversationalAwarenessChanged(bool enabled); private: QSystemTrayIcon *trayIcon; @@ -785,6 +710,12 @@ private: QByteArray lastBatteryStatus; QByteArray lastEarDetectionStatus; MediaController* mediaController; + TrayIconManager *trayManager; + + QString m_batteryStatus; + QString m_earDetectionStatus; + NoiseControlMode m_noiseControlMode = NoiseControlMode::Off; + bool m_conversationalAwareness = false; }; int main(int argc, char *argv[]) { @@ -803,47 +734,6 @@ int main(int argc, char *argv[]) { engine.rootContext()->setContextProperty("airPodsTrayApp", &trayApp); engine.loadFromModule("linux", "Main"); - QObject::connect(&trayApp, &AirPodsTrayApp::noiseControlModeChanged, &engine, [&engine](int mode) { - QObject *rootObject = engine.rootObjects().constFirst(); - - if (rootObject) { - QObject *noiseControlMode = rootObject->findChild("noiseControlMode"); - if (noiseControlMode) { - if (mode >= 0 && mode <= 3) { - QMetaObject::invokeMethod(noiseControlMode, "setCurrentIndex", Q_ARG(int, mode)); - } else { - LOG_ERROR("Invalid mode value: " << mode); - } - } - } else { - LOG_ERROR("Root object not found"); - } - }); - - QObject::connect(&trayApp, &AirPodsTrayApp::earDetectionStatusChanged, [&engine](const QString &status) { - QObject *rootObject = engine.rootObjects().first(); - if (rootObject) { - QObject *earDetectionStatus = rootObject->findChild("earDetectionStatus"); - if (earDetectionStatus) { - earDetectionStatus->setProperty("text", "Ear Detection Status: " + status); - } - } else { - LOG_ERROR("Root object not found"); - } - }); - - QObject::connect(&trayApp, &AirPodsTrayApp::batteryStatusChanged, [&engine](const QString &status) { - QObject *rootObject = engine.rootObjects().first(); - if (rootObject) { - QObject *batteryStatus = rootObject->findChild("batteryStatus"); - if (batteryStatus) { - batteryStatus->setProperty("text", "Battery Status: " + status); - } - } else { - LOG_ERROR("Root object not found"); - } - }); - return app.exec(); } diff --git a/linux/trayiconmanager.cpp b/linux/trayiconmanager.cpp new file mode 100644 index 0000000..629288e --- /dev/null +++ b/linux/trayiconmanager.cpp @@ -0,0 +1,110 @@ +#include "trayiconmanager.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace AirpodsTrayApp::Enums; + +TrayIconManager::TrayIconManager(QObject *parent) : QObject(parent) +{ + // Initialize tray icon + trayIcon = new QSystemTrayIcon(QIcon(":/icons/assets/airpods.png"), this); + trayMenu = new QMenu(); + + // Setup basic menu actions + setupMenuActions(); + + // Connect signals + trayIcon->setContextMenu(trayMenu); + connect(trayIcon, &QSystemTrayIcon::activated, this, &TrayIconManager::onTrayIconActivated); + + trayIcon->show(); +} + +void TrayIconManager::TrayIconManager::updateBatteryStatus(const QString &status) +{ + trayIcon->setToolTip("Battery Status: " + status); + updateIconFromBattery(status); +} + +void TrayIconManager::updateNoiseControlState(NoiseControlMode mode) +{ + QList actions = noiseControlGroup->actions(); + for (QAction *action : actions) + { + action->setChecked(action->data().toInt() == (int)mode); + } +} + +void TrayIconManager::updateConversationalAwareness(bool enabled) +{ + caToggleAction->setChecked(enabled); +} + +void TrayIconManager::setupMenuActions() +{ + // Conversational Awareness Toggle + caToggleAction = new QAction("Toggle Conversational Awareness", trayMenu); + caToggleAction->setCheckable(true); + trayMenu->addAction(caToggleAction); + connect(caToggleAction, &QAction::triggered, this, [this](bool checked) + { emit conversationalAwarenessToggled(checked); }); + + // Noise Control Options + noiseControlGroup = new QActionGroup(trayMenu); + const QPair noiseOptions[] = { + {"Adaptive", NoiseControlMode::Adaptive}, + {"Transparency", NoiseControlMode::Transparency}, + {"Noise Cancellation", NoiseControlMode::NoiseCancellation}, + {"Off", NoiseControlMode::Off}}; + + for (auto option : noiseOptions) + { + QAction *action = new QAction(option.first, trayMenu); + action->setCheckable(true); + action->setData((int)option.second); + noiseControlGroup->addAction(action); + trayMenu->addAction(action); + connect(action, &QAction::triggered, this, [this, mode = option.second]() + { emit noiseControlChanged(mode); }); + } + + // Quit action + QAction *quitAction = new QAction("Quit", trayMenu); + trayMenu->addAction(quitAction); + connect(quitAction, &QAction::triggered, qApp, &QApplication::quit); +} + +void TrayIconManager::updateIconFromBattery(const QString &status) +{ + QStringList parts = status.split(", "); + int leftLevel = parts[0].split(": ")[1].replace("%", "").toInt(); + int rightLevel = parts[1].split(": ")[1].replace("%", "").toInt(); + + int minLevel = (leftLevel == 0) ? rightLevel : (rightLevel == 0) ? leftLevel + : qMin(leftLevel, rightLevel); + + QPixmap pixmap(32, 32); + pixmap.fill(Qt::transparent); + QPainter painter(&pixmap); + painter.setPen(QApplication::palette().color(QPalette::WindowText)); + painter.setFont(QFont("Arial", 12, QFont::Bold)); + painter.drawText(pixmap.rect(), Qt::AlignCenter, QString::number(minLevel) + "%"); + painter.end(); + + trayIcon->setIcon(QIcon(pixmap)); +} + +void TrayIconManager::onTrayIconActivated(QSystemTrayIcon::ActivationReason reason) +{ + if (reason == QSystemTrayIcon::Trigger) + { + emit trayClicked(); + } +} \ No newline at end of file diff --git a/linux/trayiconmanager.h b/linux/trayiconmanager.h new file mode 100644 index 0000000..89c1003 --- /dev/null +++ b/linux/trayiconmanager.h @@ -0,0 +1,40 @@ +#include +#include + +#include "enums.h" + +class QMenu; +class QAction; +class QActionGroup; + +class TrayIconManager : public QObject +{ + Q_OBJECT + +public: + explicit TrayIconManager(QObject *parent = nullptr); + + void updateBatteryStatus(const QString &status); + + void updateNoiseControlState(AirpodsTrayApp::Enums::NoiseControlMode); + + void updateConversationalAwareness(bool enabled); + +private slots: + void onTrayIconActivated(QSystemTrayIcon::ActivationReason reason); + +private: + QSystemTrayIcon *trayIcon; + QMenu *trayMenu; + QAction *caToggleAction; + QActionGroup *noiseControlGroup; + + void setupMenuActions(); + + void updateIconFromBattery(const QString &status); + +signals: + void trayClicked(); + void noiseControlChanged(AirpodsTrayApp::Enums::NoiseControlMode); + void conversationalAwarenessToggled(bool enabled); +}; \ No newline at end of file