diff --git a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt index fcb8362..81cb048 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt @@ -22,10 +22,12 @@ import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context +import android.content.Context.MODE_PRIVATE import android.content.Context.RECEIVER_EXPORTED import android.content.Intent import android.content.IntentFilter import android.content.ServiceConnection +import android.content.SharedPreferences import android.os.Build import android.os.Bundle import android.os.IBinder @@ -147,6 +149,16 @@ fun Main() { } } + val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE) + val isAvailableChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == "CrossDeviceIsAvailable") { + Log.d("MainActivity", "CrossDeviceIsAvailable changed") + isRemotelyConnected.value = sharedPreferences.getBoolean("CrossDeviceIsAvailable", false) + } + } + sharedPreferences.registerOnSharedPreferenceChangeListener(isAvailableChangeListener) + Log.d("MainActivity", "CrossDeviceIsAvailable: ${sharedPreferences.getBoolean("CrossDeviceIsAvailable", false)} | isAvailable: ${CrossDevice.isAvailable}") + isRemotelyConnected.value = sharedPreferences.getBoolean("CrossDeviceIsAvailable", false) || CrossDevice.isAvailable val filter = IntentFilter().apply { addAction(AirPodsNotifications.AIRPODS_CONNECTED) addAction(AirPodsNotifications.AIRPODS_DISCONNECTED) diff --git a/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt index 92db3cb..6f35633 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt @@ -18,6 +18,7 @@ package me.kavishdevar.aln.services +import android.Manifest import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel @@ -47,6 +48,7 @@ import android.os.Looper import android.os.ParcelUuid import android.util.Log import android.widget.RemoteViews +import androidx.annotation.RequiresPermission import androidx.compose.material3.ExperimentalMaterial3Api import androidx.core.app.NotificationCompat import kotlinx.coroutines.CoroutineScope @@ -64,6 +66,7 @@ import me.kavishdevar.aln.utils.Battery import me.kavishdevar.aln.utils.BatteryComponent import me.kavishdevar.aln.utils.BatteryStatus import me.kavishdevar.aln.utils.CrossDevice +import me.kavishdevar.aln.utils.CrossDevicePackets import me.kavishdevar.aln.utils.Enums import me.kavishdevar.aln.utils.LongPressPackets import me.kavishdevar.aln.utils.MediaController @@ -163,11 +166,15 @@ class AirPodsService: Service() { return } val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a") - if (bluetoothDevice.uuids.contains(uuid)) { - val intent = Intent(AirPodsNotifications.Companion.AIRPODS_CONNECTION_DETECTED) - intent.putExtra("name", name) - intent.putExtra("device", bluetoothDevice) - context?.sendBroadcast(intent) + bluetoothDevice.fetchUuidsWithSdp() + if (bluetoothDevice.uuids != null) { + if (bluetoothDevice.uuids.contains(uuid)) { + val intent = + Intent(AirPodsNotifications.Companion.AIRPODS_CONNECTION_DETECTED) + intent.putExtra("name", name) + intent.putExtra("device", bluetoothDevice) + context?.sendBroadcast(intent) + } } } } @@ -239,6 +246,28 @@ class AirPodsService: Service() { } } + + fun sendANCBroadcast() { + sendBroadcast(Intent(AirPodsNotifications.Companion.ANC_DATA).apply { + putExtra("data", ancNotification.status) + }) + } + + fun sendBatteryBroadcast() { + sendBroadcast(Intent(AirPodsNotifications.Companion.BATTERY_DATA).apply { + putParcelableArrayListExtra("data", ArrayList(batteryNotification.getBattery())) + }) + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + fun sendBatteryNotification() { + updateNotificationContent( + true, + getSharedPreferences("settings", MODE_PRIVATE).getString("name", device?.name), + batteryNotification.getBattery() + ) + } + fun updateBatteryWidget() { val appWidgetManager = AppWidgetManager.getInstance(this) val componentName = ComponentName(this, BatteryWidget::class.java) @@ -791,20 +820,24 @@ class AirPodsService: Service() { fun sendPacket(packet: String) { val fromHex = packet.split(" ").map { it.toInt(16).toByte() } if (!isConnectedLocally && CrossDevice.isAvailable) { - CrossDevice.sendRemotePacket(fromHex.toByteArray()) + CrossDevice.sendRemotePacket(CrossDevicePackets.AIRPODS_DATA_HEADER.packet + fromHex.toByteArray()) return } - socket.outputStream?.write(fromHex.toByteArray()) - socket.outputStream?.flush() + if (this::socket.isInitialized) { + socket.outputStream?.write(fromHex.toByteArray()) + socket.outputStream?.flush() + } } - + fun sendPacket(packet: ByteArray) { if (!isConnectedLocally && CrossDevice.isAvailable) { - CrossDevice.sendRemotePacket(packet) + CrossDevice.sendRemotePacket(CrossDevicePackets.AIRPODS_DATA_HEADER.packet + packet) return } - socket.outputStream?.write(packet) - socket.outputStream?.flush() + if (this::socket.isInitialized) { + socket.outputStream?.write(packet) + socket.outputStream?.flush() + } } fun setANCMode(mode: Int) { @@ -823,37 +856,31 @@ class AirPodsService: Service() { sendPacket(Enums.NOISE_CANCELLATION_ADAPTIVE.value) } } - socket.outputStream?.flush() } fun setCAEnabled(enabled: Boolean) { sendPacket(if (enabled) Enums.SET_CONVERSATION_AWARENESS_ON.value else Enums.SET_CONVERSATION_AWARENESS_OFF.value) - socket.outputStream?.flush() } fun setOffListeningMode(enabled: Boolean) { sendPacket(byteArrayOf(0x04, 0x00 ,0x04, 0x00, 0x09, 0x00, 0x34, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00)) - socket.outputStream?.flush() } fun setAdaptiveStrength(strength: Int) { val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x2E, strength.toByte(), 0x00, 0x00, 0x00) sendPacket(bytes) - socket.outputStream?.flush() } fun setPressSpeed(speed: Int) { // 0x00 = default, 0x01 = slower, 0x02 = slowest val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x17, speed.toByte(), 0x00, 0x00, 0x00) sendPacket(bytes) - socket.outputStream?.flush() } fun setPressAndHoldDuration(speed: Int) { // 0 - default, 1 - slower, 2 - slowest val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x18, speed.toByte(), 0x00, 0x00, 0x00) sendPacket(bytes) - socket.outputStream?.flush() } fun setVolumeSwipeSpeed(speed: Int) { @@ -861,25 +888,21 @@ class AirPodsService: Service() { val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x23, speed.toByte(), 0x00, 0x00, 0x00) Log.d("AirPodsService", "Setting volume swipe speed to $speed by packet ${bytes.joinToString(" ") { "%02X".format(it) }}") sendPacket(bytes) - socket.outputStream?.flush() } fun setNoiseCancellationWithOnePod(enabled: Boolean) { val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1B, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00) sendPacket(bytes) - socket.outputStream?.flush() } fun setVolumeControl(enabled: Boolean) { val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x25, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00) sendPacket(bytes) - socket.outputStream?.flush() } fun setToneVolume(volume: Int) { val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1F, volume.toByte(), 0x50, 0x00, 0x00) sendPacket(bytes) - socket.outputStream?.flush() } val earDetectionNotification = AirPodsNotifications.EarDetection() @@ -892,7 +915,6 @@ class AirPodsService: Service() { fun setCaseChargingSounds(enabled: Boolean) { val bytes = byteArrayOf(0x12, 0x3a, 0x00, 0x01, 0x00, 0x08, if (enabled) 0x00 else 0x01) sendPacket(bytes) - socket.outputStream?.flush() } fun setEarDetection(enabled: Boolean) { diff --git a/android/app/src/main/java/me/kavishdevar/aln/utils/CrossDevice.kt b/android/app/src/main/java/me/kavishdevar/aln/utils/CrossDevice.kt index b05af4b..597d44f 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/utils/CrossDevice.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/utils/CrossDevice.kt @@ -6,6 +6,7 @@ import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothServerSocket import android.bluetooth.BluetoothSocket import android.content.Context +import android.content.SharedPreferences import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -23,6 +24,7 @@ enum class CrossDevicePackets(val packet: ByteArray) { AIRPODS_DATA_HEADER(byteArrayOf(0x00, 0x04, 0x00, 0x01)), } + object CrossDevice { private val uuid = UUID.fromString("1abbb9a4-10e4-4000-a75c-8953c5471342") private var serverSocket: BluetoothServerSocket? = null @@ -31,10 +33,12 @@ object CrossDevice { var isAvailable: Boolean = false // set to true when airpods are connected to another device var batteryBytes: ByteArray = byteArrayOf() var ancBytes: ByteArray = byteArrayOf() - + private lateinit var sharedPreferences: SharedPreferences @SuppressLint("MissingPermission") fun init(context: Context) { Log.d("AirPodsQuickSwitchService", "Initializing CrossDevice") + sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply() this.bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter startServer() } @@ -57,7 +61,8 @@ object CrossDevice { fun setAirPodsConnected(connected: Boolean) { if (connected) { - isAvailable = true + isAvailable = false + sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply() clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_CONNECTED.packet) } else { clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_DISCONNECTED.packet) @@ -69,7 +74,9 @@ object CrossDevice { clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_DATA_HEADER.packet + packet) } + @SuppressLint("MissingPermission") private fun handleClientConnection(socket: BluetoothSocket) { + Log.d("AirPodsQuickSwitchService", "Client connected") clientSocket = socket val inputStream = socket.inputStream val buffer = ByteArray(1024) @@ -78,14 +85,17 @@ object CrossDevice { while (true) { bytes = inputStream.read(buffer) val packet = buffer.copyOf(bytes) + Log.d("AirPodsQuickSwitchService", "Received packet: ${packet.joinToString("") { "%02x".format(it) }}") if (bytes == -1) { break } else if (packet.contentEquals(CrossDevicePackets.REQUEST_DISCONNECT.packet)) { ServiceManager.getService()?.disconnect() } else if (packet.contentEquals(CrossDevicePackets.AIRPODS_CONNECTED.packet)) { isAvailable = true + sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", true).apply() } else if (packet.contentEquals(CrossDevicePackets.AIRPODS_DISCONNECTED.packet)) { isAvailable = false + sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply() } else if (packet.contentEquals(CrossDevicePackets.REQUEST_BATTERY_BYTES.packet)) { Log.d("AirPodsQuickSwitchService", "Received battery request, battery data: ${batteryBytes.joinToString("") { "%02x".format(it) }}") sendRemotePacket(batteryBytes) @@ -94,16 +104,32 @@ object CrossDevice { sendRemotePacket(ancBytes) } else { - if (ServiceManager.getService()?.isConnectedLocally == true) { - val packetInHex = packet.joinToString("") { "%02x".format(it) } - ServiceManager.getService()?.sendPacket(packetInHex) - } else if (ServiceManager.getService()?.batteryNotification?.isBatteryData(packet) == true) { - batteryBytes = packet - ServiceManager.getService()?.batteryNotification?.setBattery(packet) - ServiceManager.getService()?.updateBatteryWidget() - } else if (ServiceManager.getService()?.ancNotification?.isANCData(packet) == true) { - ServiceManager.getService()?.ancNotification?.setStatus(packet) - ancBytes = packet + if (packet.sliceArray(0..3).contentEquals(CrossDevicePackets.AIRPODS_DATA_HEADER.packet)) { + // the AIRPODS_CONNECTED wasn't sent before + isAvailable = true + sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", true).apply() + val trimmedPacket = + packet.drop(CrossDevicePackets.AIRPODS_DATA_HEADER.packet.size).toByteArray() + Log.d("AirPodsQuickSwitchService", "Received relayed packet, with ${sharedPreferences.getBoolean("CrossDeviceIsAvailable", false)} | ${ServiceManager.getService()?.batteryNotification?.isBatteryData(trimmedPacket)}") + Log.d( + "AirPodsQuickSwitchService", + "Relayed packet: ${trimmedPacket.joinToString("") { "%02x".format(it) }}" + ) + if (ServiceManager.getService()?.isConnectedLocally == true) { + val packetInHex = trimmedPacket.joinToString("") { "%02x".format(it) } + ServiceManager.getService()?.sendPacket(packetInHex) + } else if (ServiceManager.getService()?.batteryNotification?.isBatteryData(trimmedPacket) == true) { + batteryBytes = trimmedPacket + ServiceManager.getService()?.batteryNotification?.setBattery(trimmedPacket) + Log.d("AirPodsQuickSwitchService", "Battery data: ${ServiceManager.getService()?.batteryNotification?.getBattery()[0]?.level}") + ServiceManager.getService()?.updateBatteryWidget() + ServiceManager.getService()?.sendBatteryBroadcast() + ServiceManager.getService()?.sendBatteryNotification() + } else if (ServiceManager.getService()?.ancNotification?.isANCData(trimmedPacket) == true) { + ServiceManager.getService()?.ancNotification?.setStatus(trimmedPacket) + ServiceManager.getService()?.sendANCBroadcast() + ancBytes = trimmedPacket + } } } } @@ -112,6 +138,7 @@ object CrossDevice { fun sendRemotePacket(byteArray: ByteArray) { if (clientSocket == null) { Log.d("AirPodsQuickSwitchService", "Client socket is null") + return } clientSocket?.outputStream?.write(byteArray) clientSocket?.outputStream?.flush() diff --git a/android/app/src/main/java/me/kavishdevar/aln/utils/Packets.kt b/android/app/src/main/java/me/kavishdevar/aln/utils/Packets.kt index b1aa27a..96cce84 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/utils/Packets.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/utils/Packets.kt @@ -22,6 +22,7 @@ package me.kavishdevar.aln.utils import android.os.Parcelable +import android.util.Log import kotlinx.parcelize.Parcelize enum class Enums(val value: ByteArray) { @@ -133,6 +134,9 @@ class AirPodsNotifications { } fun setStatus(data: ByteArray) { + if (data.size != 11) { + return + } status = data[7].toInt() } @@ -154,13 +158,17 @@ class AirPodsNotifications { fun isBatteryData(data: ByteArray): Boolean { if (data.size != 22) { + Log.d("BatteryNotification", "Battery data size is not 22") return false } - return data[0] == 0x04.toByte() && data[1] == 0x00.toByte() && data[2] == 0x04.toByte() && - data[3] == 0x00.toByte() && data[4] == 0x04.toByte() && data[5] == 0x00.toByte() + Log.d("BatteryNotification", data.joinToString("") { "%02x".format(it) }.startsWith("040004000400").toString()) + return data.joinToString("") { "%02x".format(it) }.startsWith("040004000400") } fun setBattery(data: ByteArray) { + if (data.size != 22) { + return + } first = if (data[10].toInt() == BatteryStatus.DISCONNECTED) { Battery(first.component, first.level, data[10].toInt()) } else { @@ -186,6 +194,7 @@ class AirPodsNotifications { } class ConversationalAwarenessNotification { + @Suppress("PrivatePropertyName") private val NOTIFICATION_PREFIX = Enums.CONVERSATION_AWARENESS_RECEIVE_PREFIX.value var status: Byte = 0 diff --git a/linux/crossdevice.py b/linux/crossdevice.py deleted file mode 100644 index 8e6c32e..0000000 --- a/linux/crossdevice.py +++ /dev/null @@ -1,74 +0,0 @@ -import bluetooth -import time -import threading - -# Bluetooth MAC address of the target device -TARGET_MAC = "22:22:F5:BB:1C:A0" # Replace with the actual MAC address -UUID = "1abbb9a4-10e4-4000-a75c-8953c5471342" - -# Define packets -PACKETS = { - "AIRPODS_CONNECTED": b"\x00\x01\x00\x01", - "AIRPODS_DISCONNECTED": b"\x00\x01\x00\x00", - "REQUEST_BATTERY_BYTES": b"\x00\x02\x00\x01", - "REQUEST_ANC_BYTES": b"\x00\x02\x00\x02", - "REQUEST_DISCONNECT": b"\x00\x02\x00\x00" -} - -def send_packet(sock, packet_name): - if packet_name in PACKETS: - packet = PACKETS[packet_name] - sock.send(packet) - print(f"Sent packet: {packet_name}") - else: - print(f"Packet {packet_name} not defined.") - -def listen_for_packets(sock): - try: - while True: - data = sock.recv(1024) - if data: - print(f"Received packet: {data}") - except Exception as e: - print(f"Error receiving data: {e}") - -def main(): - # Discover services to find the channel using the UUID - services = bluetooth.find_service(address=TARGET_MAC, uuid=UUID) - if len(services) == 0: - print(f"Could not find services for UUID {UUID}") - return - - # Use the first service found - service = services[0] - port = service["port"] - name = service["name"] - host = service["host"] - - print(f"Connecting to \"{name}\" on {host}, port {port}") - - # Create a Bluetooth socket - sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM) - sock.connect((host, port)) - print(f"Connected to {TARGET_MAC} on port {port}") - - # Start listening for packets in a background thread - listener_thread = threading.Thread(target=listen_for_packets, args=(sock,)) - listener_thread.daemon = True - listener_thread.start() - - try: - while True: - packet_name = input("Enter packet name to send (or 'exit' to quit): ") - if packet_name.lower() == "exit": - break - send_packet(sock, packet_name) - time.sleep(1) - except Exception as e: - print(f"Error: {e}") - finally: - sock.close() - print("Connection closed.") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/linux/main.cpp b/linux/main.cpp index 498a8c4..2935a2e 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -37,6 +37,8 @@ Q_LOGGING_CATEGORY(airpodsApp, "airpodsApp") #define LOG_ERROR(msg) qCCritical(airpodsApp) << "\033[31m" << msg << "\033[0m" #define LOG_DEBUG(msg) qCDebug(airpodsApp) << "\033[34m" << msg << "\033[0m" +#define PHONE_MAC_ADDRESS "22:22:F5:BB:1C:A0" + class AirPodsTrayApp : public QObject { Q_OBJECT @@ -104,6 +106,7 @@ public: } } initializeMprisInterface(); + connect(phoneSocket, &QBluetoothSocket::readyRead, this, &AirPodsTrayApp::onPhoneDataReceived); } public slots: @@ -340,6 +343,10 @@ public slots: qint64 bytesWritten = localSocket->write(handshakePacket); LOG_DEBUG("Handshake packet written: " << handshakePacket.toHex() << ", bytes written: " << bytesWritten); + QByteArray airpodsConnectedPacket = QByteArray::fromHex("000400010001"); + phoneSocket->write(airpodsConnectedPacket); + LOG_DEBUG("AIRPODS_CONNECTED packet written: " << airpodsConnectedPacket.toHex()); + connect(localSocket, &QBluetoothSocket::bytesWritten, this, [this, localSocket, setSpecificFeaturesPacket, requestNotificationsPacket](qint64 bytes) { LOG_INFO("Bytes written: " << bytes); if (bytes > 0) { @@ -363,6 +370,7 @@ public slots: QByteArray data = localSocket->readAll(); LOG_DEBUG("Data received: " << data.toHex()); parseData(data); + relayPacketToPhone(data); }); }); @@ -403,6 +411,7 @@ public slots: .arg(caseLevel); LOG_INFO("Battery status: " << batteryStatus); emit batteryStatusChanged(batteryStatus); + relayPacketToPhone(data); } else if (data.size() == 10 && data.startsWith(QByteArray::fromHex("040004004B00020001"))) { LOG_INFO("Received conversational awareness data"); @@ -471,6 +480,67 @@ public slots: } else { LOG_WARN("No active MPRIS media players found"); } + connectToPhone(); + } + + void connectToPhone() { + if (phoneSocket && phoneSocket->isOpen()) { + LOG_INFO("Already connected to the phone"); + return; + } + + QBluetoothAddress phoneAddress(PHONE_MAC_ADDRESS); + phoneSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol); + connect(phoneSocket, &QBluetoothSocket::connected, this, [this]() { + LOG_INFO("Connected to phone"); + }); + + 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 (phoneSocket && phoneSocket->isOpen()) { + QByteArray header = QByteArray::fromHex("00040001"); + phoneSocket->write(header + packet); + LOG_DEBUG("Relayed packet to phone with header: " << (header + packet).toHex()); + } else { + LOG_WARN("Phone socket is not open, cannot relay packet"); + } + } + + void handlePhonePacket(const QByteArray &packet) { + if (packet.startsWith(QByteArray::fromHex("00040001"))) { + QByteArray airpodsPacket = packet.mid(4); // Remove the header + 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(QByteArray::fromHex("00010001"))) { + LOG_INFO("AirPods connected"); + // Handle AirPods connected + } else if (packet.startsWith(QByteArray::fromHex("00010000"))) { + LOG_INFO("AirPods disconnected"); + // Handle AirPods disconnected + } 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()); + handlePhonePacket(data); } signals: @@ -483,6 +553,7 @@ private: QMenu *trayMenu; QBluetoothDeviceDiscoveryAgent *discoveryAgent; QBluetoothSocket *socket = nullptr; + QBluetoothSocket *phoneSocket = nullptr; QDBusInterface *mprisInterface; QString connectedDeviceMacAddress; };