#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "airpods_packets.h" #include "logger.h" #include "media/mediacontroller.h" #include "trayiconmanager.h" #include "enums.h" #include "battery.hpp" #include "BluetoothMonitor.h" #include "autostartmanager.hpp" #include "deviceinfo.hpp" #include "ble/blemanager.h" #include "ble/bleutils.h" #include "QRCodeImageProvider.hpp" #include "systemsleepmonitor.hpp" using namespace AirpodsTrayApp::Enums; Q_LOGGING_CATEGORY(librepods, "librepods") class AirPodsTrayApp : public QObject { Q_OBJECT Q_PROPERTY(bool airpodsConnected READ areAirpodsConnected NOTIFY airPodsStatusChanged) Q_PROPERTY(int earDetectionBehavior READ earDetectionBehavior WRITE setEarDetectionBehavior NOTIFY earDetectionBehaviorChanged) Q_PROPERTY(bool crossDeviceEnabled READ crossDeviceEnabled WRITE setCrossDeviceEnabled NOTIFY crossDeviceEnabledChanged) Q_PROPERTY(AutoStartManager *autoStartManager READ autoStartManager CONSTANT) Q_PROPERTY(bool notificationsEnabled READ notificationsEnabled WRITE setNotificationsEnabled NOTIFY notificationsEnabledChanged) Q_PROPERTY(int retryAttempts READ retryAttempts WRITE setRetryAttempts NOTIFY retryAttemptsChanged) Q_PROPERTY(bool hideOnStart READ hideOnStart CONSTANT) Q_PROPERTY(DeviceInfo *deviceInfo READ deviceInfo CONSTANT) Q_PROPERTY(QString phoneMacStatus READ phoneMacStatus NOTIFY phoneMacStatusChanged) Q_PROPERTY(bool hearingAidEnabled READ hearingAidEnabled WRITE setHearingAidEnabled NOTIFY hearingAidEnabledChanged) public: 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) , m_deviceInfo(new DeviceInfo(this)), m_bleManager(new BleManager(this)) , m_systemSleepMonitor(new SystemSleepMonitor(this)) { QLoggingCategory::setFilterRules(QString("librepods.debug=%1").arg(debugMode ? "true" : "false")); LOG_INFO("Initializing LibrePods"); // Initialize tray icon and connect signals trayManager = new TrayIconManager(this); trayManager->setNotificationsEnabled(loadNotificationsEnabled()); connect(trayManager, &TrayIconManager::trayClicked, this, &AirPodsTrayApp::onTrayIconActivated); connect(trayManager, &TrayIconManager::openApp, this, &AirPodsTrayApp::onOpenApp); connect(trayManager, &TrayIconManager::openSettings, this, &AirPodsTrayApp::onOpenSettings); connect(trayManager, &TrayIconManager::noiseControlChanged, this, &AirPodsTrayApp::setNoiseControlMode); connect(trayManager, &TrayIconManager::conversationalAwarenessToggled, this, &AirPodsTrayApp::setConversationalAwareness); connect(m_deviceInfo, &DeviceInfo::batteryStatusChanged, trayManager, &TrayIconManager::updateBatteryStatus); connect(m_deviceInfo, &DeviceInfo::noiseControlModeChanged, trayManager, &TrayIconManager::updateNoiseControlState); connect(m_deviceInfo, &DeviceInfo::conversationalAwarenessChanged, trayManager, &TrayIconManager::updateConversationalAwareness); connect(trayManager, &TrayIconManager::notificationsEnabledChanged, this, &AirPodsTrayApp::saveNotificationsEnabled); connect(trayManager, &TrayIconManager::notificationsEnabledChanged, this, &AirPodsTrayApp::notificationsEnabledChanged); // Initialize MediaController and connect signals mediaController = new MediaController(this); connect(mediaController, &MediaController::mediaStateChanged, this, &AirPodsTrayApp::handleMediaStateChange); mediaController->followMediaChanges(); monitor = new BluetoothMonitor(this); connect(monitor, &BluetoothMonitor::deviceConnected, this, &AirPodsTrayApp::bluezDeviceConnected); connect(monitor, &BluetoothMonitor::deviceDisconnected, this, &AirPodsTrayApp::bluezDeviceDisconnected); connect(m_bleManager, &BleManager::deviceFound, this, &AirPodsTrayApp::bleDeviceFound); connect(m_deviceInfo->getBattery(), &Battery::primaryChanged, this, &AirPodsTrayApp::primaryChanged); connect(m_systemSleepMonitor, &SystemSleepMonitor::systemGoingToSleep, this, &AirPodsTrayApp::onSystemGoingToSleep); connect(m_systemSleepMonitor, &SystemSleepMonitor::systemWakingUp, this, &AirPodsTrayApp::onSystemWakingUp); // Load settings CrossDevice.isEnabled = loadCrossDeviceEnabled(); setEarDetectionBehavior(loadEarDetectionSettings()); setRetryAttempts(loadRetryAttempts()); monitor->checkAlreadyConnectedDevices(); LOG_INFO("AirPodsTrayApp initialized"); QBluetoothLocalDevice localDevice; const QList connectedDevices = localDevice.connectedDevices(); for (const QBluetoothAddress &address : connectedDevices) { QBluetoothDeviceInfo device(address, "", 0); if (isAirPodsDevice(device)) { connectToDevice(device); // On startup after reboot, activate A2DP profile for already connected AirPods QTimer::singleShot(2000, this, [this, address]() { QString formattedAddress = address.toString().replace(":", "_"); mediaController->setConnectedDeviceMacAddress(formattedAddress); mediaController->activateA2dpProfile(); LOG_INFO("A2DP profile activation attempted for AirPods found on startup"); }); return; } } initializeDBus(); initializeBluetooth(); } ~AirPodsTrayApp() { saveCrossDeviceEnabled(); saveEarDetectionSettings(); delete socket; delete phoneSocket; } bool areAirpodsConnected() const { return socket && socket->isOpen() && socket->state() == QBluetoothSocket::SocketState::ConnectedState; } int earDetectionBehavior() const { return mediaController->getEarDetectionBehavior(); } bool crossDeviceEnabled() const { return CrossDevice.isEnabled; } AutoStartManager *autoStartManager() const { return m_autoStartManager; } bool notificationsEnabled() const { return trayManager->notificationsEnabled(); } void setNotificationsEnabled(bool enabled) { trayManager->setNotificationsEnabled(enabled); } int retryAttempts() const { return m_retryAttempts; } bool hideOnStart() const { return m_hideOnStart; } DeviceInfo *deviceInfo() const { return m_deviceInfo; } QString phoneMacStatus() const { return m_phoneMacStatus; } bool hearingAidEnabled() const { return m_deviceInfo->hearingAidEnabled(); } private: bool debugMode; bool isConnectedLocally = false; QQmlApplicationEngine *parent = nullptr; struct { bool isAvailable = true; bool isEnabled = true; // Ability to disable the feature } CrossDevice; void initializeDBus() { } bool isAirPodsDevice(const QBluetoothDeviceInfo &device) { return device.serviceUuids().contains(QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a")); } void notifyAndroidDevice() { if (!CrossDevice.isEnabled) { return; } if (phoneSocket && phoneSocket->isOpen()) { phoneSocket->write(AirPodsPackets::Phone::NOTIFICATION); LOG_DEBUG("Sent notification packet to Android: " << AirPodsPackets::Phone::NOTIFICATION.toHex()); } else { LOG_WARN("Phone socket is not open, cannot send notification packet"); } } void disconnectDevice(const QString &devicePath) { LOG_INFO("Disconnecting device at " << devicePath); } public slots: void connectToDevice(const QString &address) { LOG_INFO("Connecting to device with address: " << address); QBluetoothAddress btAddress(address); QBluetoothDeviceInfo device(btAddress, "", 0); connectToDevice(device); } void setNoiseControlMode(NoiseControlMode mode) { if (m_deviceInfo->noiseControlMode() == mode) { LOG_INFO("Noise control mode is already set to: " << static_cast(mode)); return; } LOG_INFO("Setting noise control mode to: " << mode); QByteArray packet = AirPodsPackets::NoiseControl::getPacketForMode(mode); writePacketToSocket(packet, "Noise control mode packet written: "); } void setNoiseControlModeInt(int mode) { if (mode < 0 || mode > static_cast(NoiseControlMode::Adaptive)) { LOG_ERROR("Invalid noise control mode: " << mode); return; } setNoiseControlMode(static_cast(mode)); } void setConversationalAwareness(bool enabled) { 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_deviceInfo->setConversationalAwareness(enabled); } void setOneBudANCMode(bool enabled) { if (m_deviceInfo->oneBudANCMode() == enabled) { LOG_INFO("One Bud ANC mode is already " << (enabled ? "enabled" : "disabled")); return; } LOG_INFO("Setting One Bud ANC mode to: " << (enabled ? "enabled" : "disabled")); QByteArray packet = enabled ? AirPodsPackets::OneBudANCMode::ENABLED : AirPodsPackets::OneBudANCMode::DISABLED; if (writePacketToSocket(packet, "One Bud ANC mode packet written: ")) { m_deviceInfo->setOneBudANCMode(enabled); } else { LOG_ERROR("Failed to send One Bud ANC mode command: socket not open"); } } void setRetryAttempts(int attempts) { if (m_retryAttempts != attempts) { LOG_DEBUG("Setting retry attempts to: " << attempts); m_retryAttempts = attempts; emit retryAttemptsChanged(attempts); saveRetryAttempts(attempts); } } void initiateMagicPairing() { if (!socket || !socket->isOpen()) { LOG_ERROR("Socket nicht offen, Magic Pairing kann nicht gestartet werden"); return; } writePacketToSocket(AirPodsPackets::MagicPairing::REQUEST_MAGIC_CLOUD_KEYS, "Magic Pairing packet written: "); } void setAdaptiveNoiseLevel(int level) { level = qBound(0, level, 100); if (m_deviceInfo->adaptiveNoiseLevel() != level && m_deviceInfo->adaptiveModeActive()) { QByteArray packet = AirPodsPackets::AdaptiveNoise::getPacket(level); writePacketToSocket(packet, "Adaptive noise level packet written: "); m_deviceInfo->setAdaptiveNoiseLevel(level); } } void renameAirPods(const QString &newName) { if (newName.isEmpty()) { LOG_WARN("Cannot set empty name"); return; } if (newName.size() > 32) { LOG_WARN("Name is too long, must be 32 characters or less"); return; } if (newName == m_deviceInfo->deviceName()) { LOG_INFO("Name is already set to: " << newName); return; } QByteArray packet = AirPodsPackets::Rename::getPacket(newName); if (writePacketToSocket(packet, "Rename packet written: ")) { LOG_INFO("Sent rename command for new name: " << newName); m_deviceInfo->setDeviceName(newName); } else { LOG_ERROR("Failed to send rename command: socket not open"); } } void setEarDetectionBehavior(int behavior) { if (behavior == earDetectionBehavior()) { LOG_INFO("Ear detection behavior is already set to: " << behavior); return; } mediaController->setEarDetectionBehavior(static_cast(behavior)); saveEarDetectionSettings(); emit earDetectionBehaviorChanged(behavior); } void setCrossDeviceEnabled(bool enabled) { if (CrossDevice.isEnabled == enabled) { LOG_INFO("Cross-device feature is already " << (enabled ? "enabled" : "disabled")); return; } CrossDevice.isEnabled = enabled; saveCrossDeviceEnabled(); connectToPhone(); emit crossDeviceEnabledChanged(enabled); } void setPhoneMac(const QString &mac) { if (mac.isEmpty()) { LOG_WARN("Empty MAC provided, ignoring"); m_phoneMacStatus = QStringLiteral("No MAC provided (ignoring)"); emit phoneMacStatusChanged(); return; } // Basic MAC address validation (accepts formats like AA:BB:CC:DD:EE:FF, AABBCCDDEEFF, AA-BB-CC-DD-EE-FF) QRegularExpression re("^([0-9A-Fa-f]{2}([-:]?)){5}[0-9A-Fa-f]{2}$"); if (!re.match(mac).hasMatch()) { LOG_ERROR("Invalid MAC address format: " << mac); m_phoneMacStatus = QStringLiteral("Invalid MAC: ") + mac; emit phoneMacStatusChanged(); return; } // Set environment variable for the running process qputenv("PHONE_MAC_ADDRESS", mac.toUtf8()); LOG_INFO("PHONE_MAC_ADDRESS environment variable set to: " << mac); m_phoneMacStatus = QStringLiteral("Updated MAC: ") + mac; emit phoneMacStatusChanged(); // Update QML context property so UI placeholders reflect the new value if (parent) { parent->rootContext()->setContextProperty("PHONE_MAC_ADDRESS", mac); } // If a phone socket exists, restart connection using the new MAC if (phoneSocket && phoneSocket->isOpen()) { phoneSocket->close(); phoneSocket->deleteLater(); phoneSocket = nullptr; } connectToPhone(); } void updatePhoneMacStatus(const QString &status) { m_phoneMacStatus = status; emit phoneMacStatusChanged(); } void setHearingAidEnabled(bool enabled) { LOG_INFO("Setting hearing aid to: " << (enabled ? "enabled" : "disabled")); QByteArray packet = enabled ? AirPodsPackets::HearingAid::ENABLED : AirPodsPackets::HearingAid::DISABLED; writePacketToSocket(packet, "Hearing aid packet written: "); m_deviceInfo->setHearingAidEnabled(enabled); } bool writePacketToSocket(const QByteArray &packet, const QString &logMessage) { if (socket && socket->isOpen()) { socket->write(packet); LOG_DEBUG(logMessage << packet.toHex()); return true; } else { LOG_ERROR("Socket is not open, cannot write packet"); return false; } } bool loadCrossDeviceEnabled() { return m_settings->value("crossdevice/enabled", false).toBool(); } void saveCrossDeviceEnabled() { m_settings->setValue("crossdevice/enabled", CrossDevice.isEnabled); } int loadEarDetectionSettings() { return m_settings->value("earDetection/setting", MediaController::EarDetectionBehavior::PauseWhenOneRemoved).toInt(); } void saveEarDetectionSettings() { m_settings->setValue("earDetection/setting", mediaController->getEarDetectionBehavior()); } bool loadNotificationsEnabled() const { return m_settings->value("notifications/enabled", true).toBool(); } void saveNotificationsEnabled(bool enabled) { m_settings->setValue("notifications/enabled", enabled); } int loadRetryAttempts() const { return m_settings->value("bluetooth/retryAttempts", 3).toInt(); } void saveRetryAttempts(int attempts) { m_settings->setValue("bluetooth/retryAttempts", attempts); } void onSystemGoingToSleep() { if (m_bleManager->isScanning()) { LOG_INFO("Stopping BLE scan before going to sleep"); m_bleManager->stopScan(); } } void onSystemWakingUp() { LOG_INFO("System is waking up, starting ble scan"); m_bleManager->startScan(); // Check if AirPods are already connected and activate A2DP profile if (areAirpodsConnected() && m_deviceInfo && !m_deviceInfo->bluetoothAddress().isEmpty()) { LOG_INFO("AirPods already connected after wake-up, re-activating A2DP profile"); mediaController->setConnectedDeviceMacAddress(m_deviceInfo->bluetoothAddress().replace(":", "_")); // Always activate A2DP profile after system wake since the profile might have been lost QTimer::singleShot(1000, this, [this]() { mediaController->activateA2dpProfile(); LOG_INFO("A2DP profile activation attempted after system wake-up"); }); } // Also check for already connected devices via BlueZ monitor->checkAlreadyConnectedDevices(); } private slots: void onTrayIconActivated() { QQuickWindow *window = qobject_cast( QGuiApplication::topLevelWindows().constFirst()); if (window) { window->show(); window->raise(); window->requestActivate(); } } void onOpenApp() { QObject *rootObject = parent->rootObjects().first(); if (rootObject) { QMetaObject::invokeMethod(rootObject, "reopen", Q_ARG(QVariant, "app")); } else { loadMainModule(); } } void onOpenSettings() { QObject *rootObject = parent->rootObjects().first(); if (rootObject) { QMetaObject::invokeMethod(rootObject, "reopen", Q_ARG(QVariant, "settings")); } else { loadMainModule(); } } void sendHandshake() { LOG_INFO("Connected to device, sending initial packets"); writePacketToSocket(AirPodsPackets::Connection::HANDSHAKE, "Handshake packet written: "); } void bluezDeviceConnected(const QString &address, const QString &name) { QBluetoothDeviceInfo device(QBluetoothAddress(address), name, 0); connectToDevice(device); // After system reboot, AirPods might be connected but A2DP profile not active // Attempt to activate A2DP profile after a delay to ensure connection is established QTimer::singleShot(2000, this, [this, address]() { if (!address.isEmpty()) { QString formattedAddress = address; formattedAddress = formattedAddress.replace(":", "_"); mediaController->setConnectedDeviceMacAddress(formattedAddress); mediaController->activateA2dpProfile(); LOG_INFO("A2DP profile activation attempted for newly connected device"); } }); } void onDeviceDisconnected(const QBluetoothAddress &address) { LOG_INFO("Device disconnected: " << address.toString()); if (socket) { LOG_WARN("Socket is still open, closing it"); socket->close(); socket = nullptr; } if (phoneSocket && phoneSocket->isOpen()) { phoneSocket->write(AirPodsPackets::Connection::AIRPODS_DISCONNECTED); LOG_DEBUG("AIRPODS_DISCONNECTED packet written: " << AirPodsPackets::Connection::AIRPODS_DISCONNECTED.toHex()); } // Clear the device name and model m_deviceInfo->reset(); m_bleManager->startScan(); emit airPodsStatusChanged(); // Show system notification trayManager->showNotification( tr("AirPods Disconnected"), tr("Your AirPods have been disconnected")); trayManager->resetTrayIcon(); } void bluezDeviceDisconnected(const QString &address, const QString &name) { if (address == m_deviceInfo->bluetoothAddress()) { onDeviceDisconnected(QBluetoothAddress(address)); } else { LOG_WARN("Disconnected device does not match connected device: " << address << " != " << m_deviceInfo->bluetoothAddress()); } } void parseMetadata(const QByteArray &data) { // Verify the data starts with the METADATA header if (!data.startsWith(AirPodsPackets::Parse::METADATA)) { LOG_ERROR("Invalid metadata packet: Incorrect header"); return; } int pos = AirPodsPackets::Parse::METADATA.size(); // Start after the header // Check if there is enough data to skip the initial bytes (based on example structure) if (data.size() < pos + 6) { LOG_ERROR("Metadata packet too short to parse initial bytes"); return; } pos += 6; // Skip 6 bytes after the header as per example structure auto extractString = [&data, &pos]() -> QString { if (pos >= data.size()) { return QString(); } int start = pos; while (pos < data.size() && data.at(pos) != '\0') { ++pos; } QString str = QString::fromUtf8(data.mid(start, pos - start)); if (pos < data.size()) { ++pos; // Move past the null terminator } return str; }; m_deviceInfo->setDeviceName(extractString()); m_deviceInfo->setModelNumber(extractString()); m_deviceInfo->setManufacturer(extractString()); m_deviceInfo->setModel(parseModelNumber(m_deviceInfo->modelNumber())); emit modelChanged(); // Log extracted metadata LOG_INFO("Parsed AirPods metadata:"); LOG_INFO("Device Name: " << m_deviceInfo->deviceName()); LOG_INFO("Model Number: " << m_deviceInfo->modelNumber()); LOG_INFO("Manufacturer: " << m_deviceInfo->manufacturer()); } QString getEarStatus(char value) { return (value == 0x00) ? "In Ear" : (value == 0x01) ? "Out of Ear" : "In case"; } void connectToDevice(const QBluetoothDeviceInfo &device) { if (socket && socket->isOpen() && socket->peerAddress() == device.address()) { LOG_INFO("Already connected to the device: " << device.name()); return; } LOG_INFO("Connecting to device: " << device.name()); // Clean up any existing socket if (socket) { socket->close(); socket->deleteLater(); socket = nullptr; } QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol); socket = localSocket; // Connection handler auto handleConnection = [this, localSocket]() { connect(localSocket, &QBluetoothSocket::readyRead, this, [this, localSocket]() { QByteArray data = localSocket->readAll(); QMetaObject::invokeMethod(this, "parseData", Qt::QueuedConnection, Q_ARG(QByteArray, data)); QMetaObject::invokeMethod(this, "relayPacketToPhone", Qt::QueuedConnection, Q_ARG(QByteArray, data)); }); sendHandshake(); }; // Error handler with retry auto handleError = [this, device, localSocket](QBluetoothSocket::SocketError error) { LOG_ERROR("Socket error: " << error << ", " << localSocket->errorString()); static int retryCount = 0; if (retryCount < m_retryAttempts) { retryCount++; LOG_INFO("Retrying connection (attempt " << retryCount << ")"); QTimer::singleShot(1500, this, [this, device]() { connectToDevice(device); }); } else { LOG_ERROR("Failed to connect after 3 attempts"); retryCount = 0; } }; connect(localSocket, &QBluetoothSocket::connected, this, handleConnection); connect(localSocket, QOverload::of(&QBluetoothSocket::errorOccurred), this, handleError); localSocket->connectToService(device.address(), QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a")); m_deviceInfo->setBluetoothAddress(device.address().toString()); notifyAndroidDevice(); } void parseData(const QByteArray &data) { LOG_DEBUG("Received: " << data.toHex()); if (data.startsWith(AirPodsPackets::Parse::HANDSHAKE_ACK)) { writePacketToSocket(AirPodsPackets::Connection::SET_SPECIFIC_FEATURES, "Set specific features packet written: "); } else if (data.startsWith(AirPodsPackets::Parse::FEATURES_ACK)) { writePacketToSocket(AirPodsPackets::Connection::REQUEST_NOTIFICATIONS, "Request notifications packet written: "); QTimer::singleShot(2000, this, [this]() { if (m_deviceInfo->batteryStatus().isEmpty()) { writePacketToSocket(AirPodsPackets::Connection::REQUEST_NOTIFICATIONS, "Request notifications packet written: "); } }); } // Magic Cloud Keys Response else if (data.startsWith(AirPodsPackets::MagicPairing::MAGIC_CLOUD_KEYS_HEADER)) { auto keys = AirPodsPackets::MagicPairing::parseMagicCloudKeysPacket(data); LOG_INFO("Received Magic Cloud Keys:"); LOG_INFO("MagicAccIRK: " << keys.magicAccIRK.toHex()); LOG_INFO("MagicAccEncKey: " << keys.magicAccEncKey.toHex()); // Store the keys m_deviceInfo->setMagicAccIRK(keys.magicAccIRK); m_deviceInfo->setMagicAccEncKey(keys.magicAccEncKey); m_deviceInfo->saveToSettings(*m_settings); } // Get CA state else if (data.startsWith(AirPodsPackets::ConversationalAwareness::HEADER)) { if (auto result = AirPodsPackets::ConversationalAwareness::parseState(data)) { m_deviceInfo->setConversationalAwareness(result.value()); LOG_INFO("Conversational awareness state received: " << m_deviceInfo->conversationalAwareness()); } } // Hearing Aid state else if (data.startsWith(AirPodsPackets::HearingAid::HEADER)) { if (auto result = AirPodsPackets::HearingAid::parseState(data)) { m_deviceInfo->setHearingAidEnabled(result.value()); LOG_INFO("Hearing aid state received: " << m_deviceInfo->hearingAidEnabled()); } } // Noise Control Mode else if (data.size() == 11 && data.startsWith(AirPodsPackets::NoiseControl::HEADER)) { if (auto value = AirPodsPackets::NoiseControl::parseMode(data)) { m_deviceInfo->setNoiseControlMode(value.value()); LOG_INFO("Noise control mode received: " << m_deviceInfo->noiseControlMode()); } } // Ear Detection else if (data.size() == 8 && data.startsWith(AirPodsPackets::Parse::EAR_DETECTION)) { m_deviceInfo->getEarDetection()->parseData(data); mediaController->handleEarDetection(m_deviceInfo->getEarDetection()); } // Battery Status else if ((data.size() == 22 || data.size() == 12) && data.startsWith(AirPodsPackets::Parse::BATTERY_STATUS)) { m_deviceInfo->getBattery()->parsePacket(data); m_deviceInfo->updateBatteryStatus(); LOG_INFO("Battery status: " << m_deviceInfo->batteryStatus()); } // Conversational Awareness Data else if (data.size() == 10 && data.startsWith(AirPodsPackets::ConversationalAwareness::DATA_HEADER)) { LOG_INFO("Received conversational awareness data"); mediaController->handleConversationalAwareness(data); } else if (data.startsWith(AirPodsPackets::Parse::METADATA)) { parseMetadata(data); initiateMagicPairing(); mediaController->setConnectedDeviceMacAddress(m_deviceInfo->bluetoothAddress().replace(":", "_")); if (m_deviceInfo->getEarDetection()->oneOrMorePodsInEar()) // AirPods get added as output device only after this { mediaController->activateA2dpProfile(); } m_bleManager->stopScan(); emit airPodsStatusChanged(); } else if (data.startsWith(AirPodsPackets::OneBudANCMode::HEADER)) { if (auto value = AirPodsPackets::OneBudANCMode::parseState(data)) { m_deviceInfo->setOneBudANCMode(value.value()); LOG_INFO("One Bud ANC mode received: " << m_deviceInfo->oneBudANCMode()); } } else { LOG_DEBUG("Unrecognized packet format: " << data.toHex()); } } void connectToPhone() { if (!CrossDevice.isEnabled) { return; } if (phoneSocket && phoneSocket->isOpen()) { LOG_INFO("Already connected to the phone"); return; } QBluetoothAddress phoneAddress("00:00:00:00:00:00"); // Default address, will be overwritten if PHONE_MAC_ADDRESS is set QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); if (!env.value("PHONE_MAC_ADDRESS").isEmpty()) { phoneAddress = QBluetoothAddress(env.value("PHONE_MAC_ADDRESS")); } phoneSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol); connect(phoneSocket, &QBluetoothSocket::connected, this, [this]() { LOG_INFO("Connected to phone"); if (!lastBatteryStatus.isEmpty()) { phoneSocket->write(lastBatteryStatus); LOG_DEBUG("Sent last battery status to phone: " << lastBatteryStatus.toHex()); } if (!lastEarDetectionStatus.isEmpty()) { phoneSocket->write(lastEarDetectionStatus); LOG_DEBUG("Sent last ear detection status to phone: " << lastEarDetectionStatus.toHex()); } }); connect(phoneSocket, QOverload::of(&QBluetoothSocket::errorOccurred), this, [this](QBluetoothSocket::SocketError error) { LOG_ERROR("Phone socket error: " << error << ", " << phoneSocket->errorString()); }); phoneSocket->connectToService(phoneAddress, QBluetoothUuid("1abbb9a4-10e4-4000-a75c-8953c5471342")); } void relayPacketToPhone(const QByteArray &packet) { if (!CrossDevice.isEnabled) { return; } if (phoneSocket && phoneSocket->isOpen()) { phoneSocket->write(AirPodsPackets::Phone::NOTIFICATION + packet); } else { connectToPhone(); LOG_WARN("Phone socket is not open, cannot relay packet"); } } void handlePhonePacket(const QByteArray &packet) { if (packet.startsWith(AirPodsPackets::Phone::NOTIFICATION)) { QByteArray airpodsPacket = packet.mid(4); if (socket && socket->isOpen()) { socket->write(airpodsPacket); LOG_DEBUG("Relayed packet to AirPods: " << airpodsPacket.toHex()); } else { LOG_ERROR("Socket is not open, cannot relay packet to AirPods"); } } else if (packet.startsWith(AirPodsPackets::Phone::CONNECTED)) { LOG_INFO("AirPods connected"); isConnectedLocally = true; CrossDevice.isAvailable = false; } else if (packet.startsWith(AirPodsPackets::Phone::DISCONNECTED)) { LOG_INFO("AirPods disconnected"); isConnectedLocally = false; CrossDevice.isAvailable = true; } else if (packet.startsWith(AirPodsPackets::Phone::STATUS_REQUEST)) { LOG_INFO("Connection status request received"); QByteArray response = (socket && socket->isOpen()) ? AirPodsPackets::Phone::CONNECTED : AirPodsPackets::Phone::DISCONNECTED; phoneSocket->write(response); LOG_DEBUG("Sent connection status response: " << response.toHex()); } else if (packet.startsWith(AirPodsPackets::Phone::DISCONNECT_REQUEST)) { LOG_INFO("Disconnect request received"); if (socket && socket->isOpen()) { socket->close(); LOG_INFO("Disconnected from AirPods"); QProcess process; process.start("bluetoothctl", QStringList() << "disconnect" << m_deviceInfo->bluetoothAddress()); process.waitForFinished(); QString output = process.readAllStandardOutput().trimmed(); LOG_INFO("Bluetoothctl output: " << output); isConnectedLocally = false; CrossDevice.isAvailable = true; } } else { if (socket && socket->isOpen()) { socket->write(packet); LOG_DEBUG("Relayed packet to AirPods: " << packet.toHex()); } else { LOG_ERROR("Socket is not open, cannot relay packet to AirPods"); } } } void onPhoneDataReceived() { QByteArray data = phoneSocket->readAll(); LOG_DEBUG("Data received from phone: " << data.toHex()); QMetaObject::invokeMethod(this, "handlePhonePacket", Qt::QueuedConnection, Q_ARG(QByteArray, data)); } void bleDeviceFound(const BleInfo &device) { if (BLEUtils::isValidIrkRpa(m_deviceInfo->magicAccIRK(), device.address)) { m_deviceInfo->setModel(device.modelName); auto decryptet = BLEUtils::decryptLastBytes(device.encryptedPayload, m_deviceInfo->magicAccEncKey()); m_deviceInfo->getBattery()->parseEncryptedPacket(decryptet, device.primaryLeft, device.isThisPodInTheCase, isModelHeadset(m_deviceInfo->model())); m_deviceInfo->getEarDetection()->overrideEarDetectionStatus(device.isPrimaryInEar, device.isSecondaryInEar); } } public: void handleMediaStateChange(MediaController::MediaState state) { if (state == MediaController::MediaState::Playing) { LOG_INFO("Media started playing, sending disconnect request to Android and taking over audio"); sendDisconnectRequestToAndroid(); connectToAirPods(true); } } void sendDisconnectRequestToAndroid() { if (!CrossDevice.isEnabled) return; if (phoneSocket && phoneSocket->isOpen()) { phoneSocket->write(AirPodsPackets::Phone::DISCONNECT_REQUEST); LOG_DEBUG("Sent disconnect request to Android: " << AirPodsPackets::Phone::DISCONNECT_REQUEST.toHex()); } else { LOG_WARN("Phone socket is not open, cannot send disconnect request"); } } bool isPhoneConnected() { return phoneSocket && phoneSocket->isOpen(); } 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; process.start("bluetoothctl", QStringList() << "connect" << m_deviceInfo->bluetoothAddress()); process.waitForFinished(); QString output = process.readAllStandardOutput().trimmed(); LOG_INFO("Bluetoothctl output: " << output); } QBluetoothLocalDevice localDevice; const QList connectedDevices = localDevice.connectedDevices(); for (const QBluetoothAddress &address : connectedDevices) { QBluetoothDeviceInfo device(address, "", 0); LOG_DEBUG("Connected device: " << device.name() << " (" << device.address().toString() << ")"); if (isAirPodsDevice(device)) { connectToDevice(device); return; } } LOG_WARN("AirPods not found among connected devices"); } void initializeBluetooth() { connectToPhone(); m_deviceInfo->loadFromSettings(*m_settings); if (!areAirpodsConnected()) { m_bleManager->startScan(); } } void loadMainModule() { parent->load(QUrl(QStringLiteral("qrc:/linux/Main.qml"))); } signals: void noiseControlModeChanged(NoiseControlMode mode); void earDetectionStatusChanged(const QString &status); void batteryStatusChanged(const QString &status); void conversationalAwarenessChanged(bool enabled); void adaptiveNoiseLevelChanged(int level); void deviceNameChanged(const QString &name); void modelChanged(); void primaryChanged(); void airPodsStatusChanged(); void earDetectionBehaviorChanged(int behavior); void crossDeviceEnabledChanged(bool enabled); void notificationsEnabledChanged(bool enabled); void retryAttemptsChanged(int attempts); void oneBudANCModeChanged(bool enabled); void phoneMacStatusChanged(); void hearingAidEnabledChanged(bool enabled); private: QBluetoothSocket *socket = nullptr; QBluetoothSocket *phoneSocket = nullptr; QByteArray lastBatteryStatus; QByteArray lastEarDetectionStatus; MediaController* mediaController; TrayIconManager *trayManager; BluetoothMonitor *monitor; QSettings *m_settings; AutoStartManager *m_autoStartManager; int m_retryAttempts = 3; bool m_hideOnStart = false; DeviceInfo *m_deviceInfo; BleManager *m_bleManager; SystemSleepMonitor *m_systemSleepMonitor = nullptr; QString m_phoneMacStatus; }; int main(int argc, char *argv[]) { QApplication app(argc, argv); // Load translations QTranslator *translator = new QTranslator(&app); QString locale = QLocale::system().name(); // Try to load translation from various locations QStringList translationPaths = { QCoreApplication::applicationDirPath() + "/translations", QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/librepods/translations", "/usr/share/librepods/translations", "/usr/local/share/librepods/translations" }; for (const QString &path : translationPaths) { if (translator->load("librepods_" + locale, path)) { app.installTranslator(translator); break; } } QLocalServer::removeServer("app_server"); QFile stale("/tmp/app_server"); if (stale.exists()) stale.remove(); QLocalSocket socket_check; socket_check.connectToServer("app_server"); if (socket_check.waitForConnected(300)) { LOG_INFO("Another instance already running! Reopening window..."); socket_check.write("reopen"); socket_check.flush(); socket_check.waitForBytesWritten(200); socket_check.disconnectFromServer(); return 0; } app.setDesktopFileName("me.kavishdevar.librepods"); app.setQuitOnLastWindowClosed(false); bool debugMode = false; bool hideOnStart = false; for (int i = 1; i < argc; ++i) { if (QString(argv[i]) == "--debug") debugMode = true; if (QString(argv[i]) == "--hide") hideOnStart = true; } QQmlApplicationEngine engine; qmlRegisterType("me.kavishdevar.Battery", 1, 0, "Battery"); qmlRegisterType("me.kavishdevar.DeviceInfo", 1, 0, "DeviceInfo"); AirPodsTrayApp *trayApp = new AirPodsTrayApp(debugMode, hideOnStart, &engine); engine.rootContext()->setContextProperty("airPodsTrayApp", trayApp); // Expose PHONE_MAC_ADDRESS environment variable to QML for placeholder in settings { QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QString phoneMacEnv = env.value("PHONE_MAC_ADDRESS", ""); engine.rootContext()->setContextProperty("PHONE_MAC_ADDRESS", phoneMacEnv); // Initialize the visible status in the GUI trayApp->updatePhoneMacStatus(phoneMacEnv.isEmpty() ? QStringLiteral("No phone MAC set") : phoneMacEnv); } engine.addImageProvider("qrcode", new QRCodeImageProvider()); trayApp->loadMainModule(); QLocalServer server; QLocalServer::removeServer("app_server"); if (!server.listen("app_server")) { LOG_ERROR("Unable to start the listening server"); LOG_DEBUG("Server error: " << server.errorString()); } else { LOG_DEBUG("Server started, waiting for connections..."); } QObject::connect(&server, &QLocalServer::newConnection, [&]() { QLocalSocket* socket = server.nextPendingConnection(); // Handles Proper Connection QObject::connect(socket, &QLocalSocket::readyRead, [socket, &engine, &trayApp]() { QString msg = socket->readAll(); // Check if the message is "reopen", if so, trigger onOpenApp function if (msg == "reopen") { LOG_INFO("Reopening app window"); QObject *rootObject = engine.rootObjects().first(); if (rootObject) { QMetaObject::invokeMethod(rootObject, "reopen", Q_ARG(QVariant, "app")); } else { trayApp->loadMainModule(); } } else { LOG_ERROR("Unknown message received: " << msg); } socket->disconnectFromServer(); }); // Handles connection errors QObject::connect(socket, &QLocalSocket::errorOccurred, [socket]() { LOG_ERROR("Failed to connect to the duplicate app instance"); LOG_DEBUG("Connection error: " << socket->errorString()); }); // Handle server-level errors QObject::connect(&server, &QLocalServer::serverError, [&]() { LOG_ERROR("Server failed to accept a new connection"); LOG_DEBUG("Server error: " << server.errorString()); }); }); QObject::connect(&app, &QCoreApplication::aboutToQuit, [&]() { LOG_DEBUG("Application quitting. Cleaning up local server..."); if (server.isListening()) { server.close(); } QLocalServer::removeServer("app_server"); QFile stale("/tmp/app_server"); if (stale.exists()) stale.remove(); }); return app.exec(); } #include "main.moc"