diff --git a/linux/Main.qml b/linux/Main.qml index a756d48..08b62cb 100644 --- a/linux/Main.qml +++ b/linux/Main.qml @@ -2,11 +2,16 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 ApplicationWindow { + id: mainWindow visible: true width: 400 height: 300 title: "AirPods Settings" + onClosing: function(event) { + mainWindow.visible = false + } + Column { anchors.left: parent.left anchors.right: parent.right diff --git a/linux/airpods_packets.h b/linux/airpods_packets.h index e606e02..8148e92 100644 --- a/linux/airpods_packets.h +++ b/linux/airpods_packets.h @@ -38,10 +38,27 @@ namespace AirPodsPackets // Conversational Awareness Packets namespace ConversationalAwareness { - static const QByteArray HEADER = QByteArray::fromHex("04000400090028"); // Added for parsing - static const QByteArray ENABLED = HEADER + QByteArray::fromHex("01000000"); - static const QByteArray DISABLED = HEADER + QByteArray::fromHex("02000000"); - static const QByteArray DATA_HEADER = QByteArray::fromHex("040004004B00020001"); // For received data + static const QByteArray HEADER = QByteArray::fromHex("04000400090028"); // For command/status + static const QByteArray ENABLED = HEADER + QByteArray::fromHex("01000000"); // Command to enable + static const QByteArray DISABLED = HEADER + QByteArray::fromHex("02000000"); // Command to disable + static const QByteArray DATA_HEADER = QByteArray::fromHex("040004004B00020001"); // For received speech level data + + static std::optional parseCAState(const QByteArray &data) + { + // Extract the status byte (index 7) + quint8 statusByte = static_cast(data.at(HEADER.size())); // HEADER.size() is 7 + + // Interpret the status byte + switch (statusByte) + { + case 0x01: // Enabled + return true; + case 0x02: // Disabled + return false; + default: + return std::nullopt; + } + } } // Connection Packets @@ -88,12 +105,91 @@ namespace AirPodsPackets } } + namespace MagicPairing { + static const QByteArray REQUEST_MAGIC_CLOUD_KEYS = QByteArray::fromHex("0400040030000500"); + static const QByteArray MAGIC_CLOUD_KEYS_HEADER = QByteArray::fromHex("04000400310002"); + + struct MagicCloudKeys { + QByteArray magicAccIRK; // 16 bytes + QByteArray magicAccEncKey; // 16 bytes + }; + + inline MagicCloudKeys parseMagicCloudKeysPacket(const QByteArray &data) + { + MagicCloudKeys keys; + + // Expected size: header (7 bytes) + (1 (tag) + 2 (length) + 1 (reserved) + 16 (value)) * 2 = 47 bytes. + if (data.size() < 47) + { + return keys; // or handle error as needed + } + + // Check header + if (!data.startsWith(MAGIC_CLOUD_KEYS_HEADER)) + { + return keys; // header mismatch + } + + int index = MAGIC_CLOUD_KEYS_HEADER.size(); // Start after header (index 7) + + // --- TLV Block 1 (MagicAccIRK) --- + // Tag should be 0x01 + if (static_cast(data.at(index)) != 0x01) + { + return keys; // unexpected tag + } + index += 1; + + // Read length (2 bytes, big-endian) + quint16 len1 = (static_cast(data.at(index)) << 8) | static_cast(data.at(index + 1)); + if (len1 != 16) + { + return keys; // invalid length + } + index += 2; + + // Skip reserved byte + index += 1; + + // Extract MagicAccIRK (16 bytes) + keys.magicAccIRK = data.mid(index, 16); + index += 16; + + // --- TLV Block 2 (MagicAccEncKey) --- + // Tag should be 0x04 + if (static_cast(data.at(index)) != 0x04) + { + return keys; // unexpected tag + } + index += 1; + + // Read length (2 bytes, big-endian) + quint16 len2 = (static_cast(data.at(index)) << 8) | static_cast(data.at(index + 1)); + if (len2 != 16) + { + return keys; // invalid length + } + index += 2; + + // Skip reserved byte + index += 1; + + // Extract MagicAccEncKey (16 bytes) + keys.magicAccEncKey = data.mid(index, 16); + index += 16; + + return keys; + } + } + // Parsing Headers namespace Parse { static const QByteArray EAR_DETECTION = QByteArray::fromHex("040004000600"); static const QByteArray BATTERY_STATUS = QByteArray::fromHex("040004000400"); static const QByteArray METADATA = QByteArray::fromHex("040004001d"); + static const QByteArray HANDSHAKE_ACK = QByteArray::fromHex("01000400"); + static const QByteArray FEATURES_ACK = QByteArray::fromHex("040004002b00"); // Note: Only tested with airpods pro 2 } } diff --git a/linux/ble/blescanner.cpp b/linux/ble/blescanner.cpp index 2ce149d..83ba15a 100644 --- a/linux/ble/blescanner.cpp +++ b/linux/ble/blescanner.cpp @@ -90,6 +90,7 @@ BleScanner::BleScanner(QWidget *parent) : QMainWindow(parent) detailsLayout->addWidget(new QLabel("Raw Data:"), 7, 0); rawDataLabel = new QLabel(this); rawDataLabel->setWordWrap(true); + rawDataLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); detailsLayout->addWidget(rawDataLabel, 7, 1, 1, 2); // New Rows for Additional Info diff --git a/linux/main.cpp b/linux/main.cpp index 5dad02d..cf2f59b 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -57,8 +57,7 @@ public: connect(m_battery, &Battery::primaryChanged, this, &AirPodsTrayApp::primaryChanged); - // load conversational awareness state - setConversationalAwareness(loadConversationalAwarenessState()); + CrossDevice.isEnabled = loadCrossDeviceEnabled(); discoveryAgent = new QBluetoothDeviceDiscoveryAgent(); discoveryAgent->setLowEnergyDiscoveryTimeout(15000); @@ -69,8 +68,6 @@ public: LOG_INFO("AirPodsTrayApp initialized and started device discovery"); QBluetoothLocalDevice localDevice; - connect(&localDevice, &QBluetoothLocalDevice::deviceConnected, this, &AirPodsTrayApp::onDeviceConnected); - connect(&localDevice, &QBluetoothLocalDevice::deviceDisconnected, this, &AirPodsTrayApp::onDeviceDisconnected); const QList connectedDevices = localDevice.connectedDevices(); for (const QBluetoothAddress &address : connectedDevices) { @@ -80,7 +77,6 @@ public: return; } } - connect(phoneSocket, &QBluetoothSocket::readyRead, this, &AirPodsTrayApp::onPhoneDataReceived); QDBusInterface iface("org.bluez", "/org/bluez", "org.bluez.Adapter1"); QDBusReply reply = iface.call("GetServiceRecords", QString::fromUtf8("74ec2172-0bad-4d01-8f77-997b2be0722a")); @@ -96,6 +92,8 @@ public: } ~AirPodsTrayApp() { + saveCrossDeviceEnabled(); + delete trayIcon; delete trayMenu; delete discoveryAgent; @@ -136,6 +134,7 @@ private: bool isConnectedLocally = false; struct { bool isAvailable = true; + bool isEnabled = true; // Ability to disable the feature } CrossDevice; void initializeDBus() { @@ -287,7 +286,17 @@ public slots: writePacketToSocket(packet, "Conversational awareness packet written: "); m_conversationalAwareness = enabled; emit conversationalAwarenessChanged(enabled); - saveConversationalAwarenessState(); + } + + 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) @@ -348,16 +357,16 @@ public slots: } } - bool loadConversationalAwarenessState() + bool loadCrossDeviceEnabled() { QSettings settings; - return settings.value("conversationalAwareness", false).toBool(); + return settings.value("crossdevice/enabled", false).toBool(); } - void saveConversationalAwarenessState() + void saveCrossDeviceEnabled() { QSettings settings; - settings.setValue("conversationalAwareness", m_conversationalAwareness); + settings.setValue("crossdevice/enabled", CrossDevice.isEnabled); settings.sync(); } @@ -374,6 +383,12 @@ private slots: } } + void sendHandshake() { + LOG_INFO("Connected to device, sending initial packets"); + discoveryAgent->stop(); + writePacketToSocket(AirPodsPackets::Connection::HANDSHAKE, "Handshake packet written: "); + } + void onDeviceDiscovered(const QBluetoothDeviceInfo &device) { QByteArray manufacturerData = device.manufacturerData(MANUFACTURER_ID); if (manufacturerData.startsWith(MANUFACTURER_DATA)) { @@ -509,38 +524,21 @@ private slots: LOG_INFO("Connecting to device: " << device.name()); QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol); + connect(localSocket, &QBluetoothSocket::disconnected, this, [this, localSocket]() { + onDeviceDisconnected(localSocket->peerAddress()); + }); connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() { - LOG_INFO("Connected to device, sending initial packets"); - discoveryAgent->stop(); - - QByteArray handshakePacket = AirPodsPackets::Connection::HANDSHAKE; - QByteArray setSpecificFeaturesPacket = AirPodsPackets::Connection::SET_SPECIFIC_FEATURES; - QByteArray requestNotificationsPacket = AirPodsPackets::Connection::REQUEST_NOTIFICATIONS; - - qint64 bytesWritten = localSocket->write(handshakePacket); - LOG_DEBUG("Handshake packet written: " << handshakePacket.toHex() << ", bytes written: " << bytesWritten); - localSocket->write(setSpecificFeaturesPacket); - LOG_DEBUG("Set specific features packet written: " << setSpecificFeaturesPacket.toHex()); - localSocket->write(requestNotificationsPacket); - LOG_DEBUG("Request notifications packet written: " << requestNotificationsPacket.toHex()); - connect(localSocket, &QBluetoothSocket::bytesWritten, this, [this, localSocket, setSpecificFeaturesPacket, requestNotificationsPacket](qint64 bytes) { - LOG_INFO("Bytes written: " << bytes); - if (bytes > 0) { - static int step = 0; - switch (step) { - case 0: - localSocket->write(setSpecificFeaturesPacket); - LOG_DEBUG("Set specific features packet written: " << setSpecificFeaturesPacket.toHex()); - step++; - break; - case 1: - localSocket->write(requestNotificationsPacket); - LOG_DEBUG("Request notifications packet written: " << requestNotificationsPacket.toHex()); - step++; - break; - } + // Start periodic magic pairing attempts + QTimer *magicPairingTimer = new QTimer(this); + connect(magicPairingTimer, &QTimer::timeout, this, [this, magicPairingTimer]() { + if (m_magicAccIRK.isEmpty() || m_magicAccEncKey.isEmpty()) { + initiateMagicPairing(); + } else { + magicPairingTimer->stop(); + magicPairingTimer->deleteLater(); } }); + magicPairingTimer->start(500); connect(localSocket, &QBluetoothSocket::readyRead, this, [this, localSocket]() { QByteArray data = localSocket->readAll(); @@ -548,24 +546,15 @@ private slots: QMetaObject::invokeMethod(this, "relayPacketToPhone", Qt::QueuedConnection, Q_ARG(QByteArray, data)); }); - QTimer::singleShot(500, this, [localSocket, setSpecificFeaturesPacket, requestNotificationsPacket]() { - if (localSocket->isOpen()) { - localSocket->write(setSpecificFeaturesPacket); - LOG_DEBUG("Resent set specific features packet: " << setSpecificFeaturesPacket.toHex()); - localSocket->write(requestNotificationsPacket); - LOG_DEBUG("Resent request notifications packet: " << requestNotificationsPacket.toHex()); - } else { - LOG_WARN("Socket is not open, cannot resend packets"); - } - }); + sendHandshake(); }); connect(localSocket, QOverload::of(&QBluetoothSocket::errorOccurred), this, [this, localSocket](QBluetoothSocket::SocketError error) { LOG_ERROR("Socket error: " << error << ", " << localSocket->errorString()); }); - localSocket->connectToService(device.address(), QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a")); socket = localSocket; + localSocket->connectToService(device.address(), QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a")); connectedDeviceMacAddress = device.address().toString().replace(":", "_"); mediaController->setConnectedDeviceMacAddress(connectedDeviceMacAddress); notifyAndroidDevice(); @@ -581,8 +570,45 @@ private slots: { LOG_DEBUG("Received: " << data.toHex()); + if (data.startsWith(AirPodsPackets::Parse::HANDSHAKE_ACK)) + { + writePacketToSocket(AirPodsPackets::Connection::SET_SPECIFIC_FEATURES, "Set specific features packet written: "); + } + if (data.startsWith(AirPodsPackets::Parse::FEATURES_ACK)) + { + writePacketToSocket(AirPodsPackets::Connection::REQUEST_NOTIFICATIONS, "Request notifications packet written: "); + + QTimer::singleShot(2000, this, [this]() { + if (m_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 for later use if needed + m_magicAccIRK = keys.magicAccIRK; + m_magicAccEncKey = keys.magicAccEncKey; + } + // Get CA state + else if (data.startsWith(AirPodsPackets::ConversationalAwareness::HEADER)) { + auto result = AirPodsPackets::ConversationalAwareness::parseCAState(data); + if (result.has_value()) { + m_conversationalAwareness = result.value(); + LOG_INFO("Conversational awareness state received: " << m_conversationalAwareness); + emit conversationalAwarenessChanged(m_conversationalAwareness); + } else { + LOG_ERROR("Failed to parse conversational awareness state"); + } + } // Noise Control Mode - if (data.size() == 11 && data.startsWith(AirPodsPackets::NoiseControl::HEADER)) + else if (data.size() == 11 && data.startsWith(AirPodsPackets::NoiseControl::HEADER)) { quint8 rawMode = data[7] - 1; // Offset still needed due to protocol if (rawMode >= (int)NoiseControlMode::MinValue && rawMode <= (int)NoiseControlMode::MaxValue) @@ -643,6 +669,10 @@ private slots: } void connectToPhone() { + if (!CrossDevice.isEnabled) { + return; + } + if (phoneSocket && phoneSocket->isOpen()) { LOG_INFO("Already connected to the phone"); return; @@ -671,6 +701,9 @@ private slots: void relayPacketToPhone(const QByteArray &packet) { + if (!CrossDevice.isEnabled) { + return; + } if (phoneSocket && phoneSocket->isOpen()) { phoneSocket->write(AirPodsPackets::Phone::NOTIFICATION + packet); @@ -876,6 +909,7 @@ private: QByteArray lastEarDetectionStatus; MediaController* mediaController; TrayIconManager *trayManager; + QSettings *settings; QString m_batteryStatus; QString m_earDetectionStatus; @@ -887,10 +921,13 @@ private: AirPodsModel m_model = AirPodsModel::Unknown; bool m_primaryInEar = false; bool m_secoundaryInEar = false; + QByteArray m_magicAccIRK; + QByteArray m_magicAccEncKey; }; int main(int argc, char *argv[]) { QApplication app(argc, argv); + app.setQuitOnLastWindowClosed(false); bool debugMode = false; for (int i = 1; i < argc; ++i) {