From 28c318f9e0463c17292e5a11c9db009737e022f1 Mon Sep 17 00:00:00 2001 From: beeemT Date: Sat, 25 Jul 2026 16:26:13 +0200 Subject: [PATCH 1/5] Keep AirPods AACP configuration synchronized across reconnects --- .../librepods/bluetooth/AACPManager.kt | 70 +-- .../librepods/services/AirPodsService.kt | 591 ++++++++++++------ 2 files changed, 429 insertions(+), 232 deletions(-) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt index ac6d356b7..74b632fa2 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt @@ -246,6 +246,7 @@ class AACPManager { fun onHeadphoneAccommodationReceived(eqData: FloatArray) fun onCustomEqReceived(customEq: CustomEq) fun onCapabilitiesReceived(capabilities: List) + fun onAACPFrameReceived() {} } fun parseStemPressResponse(data: ByteArray): Pair { @@ -307,40 +308,28 @@ class AACPManager { } fun sendControlCommand(identifier: Byte, value: ByteArray): Boolean { - val controlPacket = createControlCommandPacket(identifier, value) - setControlCommandStatusValue( - ControlCommandIdentifiers.fromByte(identifier) ?: return false, value - ) - return sendDataPacket(controlPacket) + if (ControlCommandIdentifiers.fromByte(identifier) == null) return false + return sendDataPacket(createControlCommandPacket(identifier, value)) } @OptIn(ExperimentalStdlibApi::class) fun sendControlCommand(identifier: Byte, value: Byte): Boolean { - val controlPacket = createControlCommandPacket(identifier, byteArrayOf(value)) - setControlCommandStatusValue( - ControlCommandIdentifiers.fromByte(identifier) ?: return false, byteArrayOf(value) - ) - return sendDataPacket(controlPacket) + if (ControlCommandIdentifiers.fromByte(identifier) == null) return false + return sendDataPacket(createControlCommandPacket(identifier, byteArrayOf(value))) } fun sendControlCommand(identifier: Byte, value: Boolean): Boolean { - val controlPacket = createControlCommandPacket( - identifier, if (value) byteArrayOf(0x01) else byteArrayOf(0x02) - ) - setControlCommandStatusValue( - ControlCommandIdentifiers.fromByte(identifier) ?: return false, - if (value) byteArrayOf(0x01) else byteArrayOf(0x02) + if (ControlCommandIdentifiers.fromByte(identifier) == null) return false + return sendDataPacket( + createControlCommandPacket( + identifier, if (value) byteArrayOf(0x01) else byteArrayOf(0x02) + ) ) - return sendDataPacket(controlPacket) } fun sendControlCommand(identifier: Byte, value: Int): Boolean { - val controlPacket = createControlCommandPacket(identifier, byteArrayOf(value.toByte())) - setControlCommandStatusValue( - ControlCommandIdentifiers.fromByte(identifier) ?: return false, - byteArrayOf(value.toByte()) - ) - return sendDataPacket(controlPacket) + if (ControlCommandIdentifiers.fromByte(identifier) == null) return false + return sendDataPacket(createControlCommandPacket(identifier, byteArrayOf(value.toByte()))) } fun parseProximityKeysResponse(data: ByteArray): Map { @@ -414,6 +403,7 @@ class AACPManager { ) return } + callback?.onAACPFrameReceived() when (val opcode = packet[4]) { Opcodes.BATTERY_INFO -> { @@ -1141,34 +1131,41 @@ class AACPManager { try { Log.d(TAG, "Sending packet: ${packet.joinToString(" ") { "%02X".format(it) }}") - if (packet[4] == Opcodes.CONTROL_COMMAND) { - val controlCommand = try { + val controlCommand = if (packet.size > 4 && packet[4] == Opcodes.CONTROL_COMMAND) { + try { ControlCommand.fromByteArray(packet) } catch (e: Exception) { Log.w(TAG, "Invalid control command: ${e.message}") callback?.onUnknownPacketReceived(packet) return false } + } else { + null + } + + if (controlCommand != null) { Log.d( TAG, "Control command: ${controlCommand.identifier.toHexString()} - ${ controlCommand.value.joinToString(" ") { "%02X".format(it) } }") - setControlCommandStatusValue( - ControlCommandIdentifiers.fromByte(controlCommand.identifier) ?: return false, - controlCommand.value - ) } val socket = BluetoothConnectionManager.aacpSocket ?: return false - - if (socket.isConnected) { - socket.outputStream?.write(packet) - socket.outputStream?.flush() - return true - } else { + if (!socket.isConnected) { Log.d(TAG, "Can't send packet: Socket not initialized or connected") return false } + + socket.outputStream?.write(packet) + socket.outputStream?.flush() + + if (controlCommand != null) { + setControlCommandStatusValue( + ControlCommandIdentifiers.fromByte(controlCommand.identifier) ?: return false, + controlCommand.value + ) + } + return true } catch (e: Exception) { Log.e(TAG, "Error sending packet: ${e.message}") return false @@ -1270,9 +1267,8 @@ class AACPManager { } fun disconnected() { - Log.d(TAG, "Disconnected, clearing state") + Log.d(TAG, "Disconnected, clearing session state") controlCommandStatusList.clear() - controlCommandListeners.clear() owns = false oldConnectedDevices = listOf() connectedDevices = listOf() diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt index 0cf08c11d..e945db979 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt @@ -71,6 +71,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.core.app.NotificationCompat import androidx.core.content.edit import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -160,6 +163,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList var cameraActive = false private var disconnectedBecauseReversed = false private var otherDeviceTookOver = false + private val aacpSessionLock = Any() + private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var aacpSessionGeneration = 0L + private var connectionAttemptInProgress = false + private var pendingAacpSocket: BluetoothSocket? = null + private var aacpReaderJob: Job? = null + private var aacpInitializationJob: Job? = null + private var readyAacpSessionGeneration: Long? = null data class ServiceConfig( var deviceName: String = "AirPods", @@ -696,9 +707,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList // isConnectedLocally = false popupShown = false updateNotificationContent(false) - aacpManager.disconnected() - BluetoothConnectionManager.aacpSocket = null - BluetoothConnectionManager.attSocket = null + invalidateAacpSession() } } } @@ -818,7 +827,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList return action != default } - fun setupStemActions() { + fun setupStemActions(): Boolean { val singlePressDefault = StemAction.defaultActions[StemPressType.SINGLE_PRESS] val doublePressDefault = StemAction.defaultActions[StemPressType.DOUBLE_PRESS] val triplePressDefault = StemAction.defaultActions[StemPressType.TRIPLE_PRESS] @@ -845,7 +854,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList TAG, "Setting up stem actions: Single Press Customized: $singlePressCustomized, Double Press Customized: $doublePressCustomized, Triple Press Customized: $triplePressCustomized, Long Press Customized: $longPressCustomized" ) - aacpManager.sendStemConfigPacket( + return aacpManager.sendStemConfigPacket( singlePressCustomized, doublePressCustomized, triplePressCustomized, @@ -1176,6 +1185,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList // TODO } + override fun onAACPFrameReceived() { + this@AirPodsService.onAACPFrameReceived() + } override fun onUnknownPacketReceived(packet: ByteArray) { Log.d( "AACPManager", @@ -2633,216 +2645,407 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList // CrossDevice.isAvailable = false } + private fun beginAacpConnection(): Long? { + var staleSockets: List = emptyList() + val generation = synchronized(aacpSessionLock) { + if (connectionAttemptInProgress || BluetoothConnectionManager.aacpSocket?.isConnected == true) { + return@synchronized null + } + + connectionAttemptInProgress = true + aacpSessionGeneration += 1 + readyAacpSessionGeneration = null + aacpInitializationJob?.cancel() + aacpInitializationJob = null + aacpReaderJob?.cancel() + aacpReaderJob = null + staleSockets = listOfNotNull( + BluetoothConnectionManager.aacpSocket, + BluetoothConnectionManager.attSocket + ).distinct() + BluetoothConnectionManager.aacpSocket = null + BluetoothConnectionManager.attSocket = null + pendingAacpSocket = null + aacpSessionGeneration + } + staleSockets.forEach { socket -> + runCatching { socket.close() } + } + return generation + } + + private fun isCurrentAacpGeneration(generation: Long): Boolean { + return synchronized(aacpSessionLock) { + aacpSessionGeneration == generation + } + } + + private fun activateAacpSession( + generation: Long, + socket: BluetoothSocket, + attSocket: BluetoothSocket? + ): Boolean { + return synchronized(aacpSessionLock) { + if (aacpSessionGeneration != generation || !connectionAttemptInProgress) { + false + } else { + pendingAacpSocket = null + BluetoothConnectionManager.aacpSocket = socket + BluetoothConnectionManager.attSocket = attSocket + connectionAttemptInProgress = false + true + } + } + } + + private fun invalidateAacpSession() { + var sockets: List = emptyList() + var readerJob: Job? = null + var initializationJob: Job? = null + synchronized(aacpSessionLock) { + aacpSessionGeneration += 1 + connectionAttemptInProgress = false + readyAacpSessionGeneration = null + readerJob = aacpReaderJob + initializationJob = aacpInitializationJob + sockets = listOfNotNull( + pendingAacpSocket, + BluetoothConnectionManager.aacpSocket, + BluetoothConnectionManager.attSocket + ).distinct() + pendingAacpSocket = null + aacpReaderJob = null + aacpInitializationJob = null + BluetoothConnectionManager.aacpSocket = null + BluetoothConnectionManager.attSocket = null + } + readerJob?.cancel() + initializationJob?.cancel() + sockets.forEach { socket -> + runCatching { socket.close() } + } + aacpManager.disconnected() + } + + private fun finishAacpSession(generation: Long, socket: BluetoothSocket) { + var sockets: List = emptyList() + var initializationJob: Job? = null + val shouldNotify = synchronized(aacpSessionLock) { + if (aacpSessionGeneration != generation || + BluetoothConnectionManager.aacpSocket !== socket + ) { + false + } else { + aacpSessionGeneration += 1 + connectionAttemptInProgress = false + readyAacpSessionGeneration = null + initializationJob = aacpInitializationJob + sockets = listOfNotNull( + socket, + BluetoothConnectionManager.attSocket + ).distinct() + aacpReaderJob = null + aacpInitializationJob = null + BluetoothConnectionManager.aacpSocket = null + BluetoothConnectionManager.attSocket = null + true + } + } + if (!shouldNotify) return + + initializationJob?.cancel() + sockets.forEach { currentSocket -> + runCatching { currentSocket.close() } + } + aacpManager.disconnected() + updateNotificationContent(false) + sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply { + setPackage(packageName) + }) + } + + private fun onAACPFrameReceived() { + val generation = synchronized(aacpSessionLock) { + if (BluetoothConnectionManager.aacpSocket == null || + readyAacpSessionGeneration == aacpSessionGeneration + ) { + null + } else { + aacpSessionGeneration + } + } ?: return + + val listeningModes = sharedPreferences.getInt("long_press_byte", 0b0111) + .coerceIn(0, 0xFF) + .toByte() + val allowOffMode = sharedPreferences.getBoolean("off_listening_mode", true) + val sent = synchronized(aacpSessionLock) { + if (aacpSessionGeneration != generation || + BluetoothConnectionManager.aacpSocket == null + ) { + null + } else { + val listeningModesSent = aacpManager.sendControlCommand( + AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS.value, + listeningModes + ) + val allowOffModeSent = aacpManager.sendControlCommand( + AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION.value, + allowOffMode + ) + val stemConfigSent = setupStemActions() + Triple(listeningModesSent, allowOffModeSent, stemConfigSent) + } + } ?: return + + val (listeningModesSent, allowOffModeSent, stemConfigSent) = sent + val allConfigurationSent = + listeningModesSent && allowOffModeSent && stemConfigSent + if (!allConfigurationSent) { + Log.w( + TAG, + "AACP session $generation configuration write failed; retrying on next frame" + ) + return + } + val ready = synchronized(aacpSessionLock) { + if (aacpSessionGeneration == generation && + BluetoothConnectionManager.aacpSocket != null + ) { + readyAacpSessionGeneration = generation + true + } else { + false + } + } + if (!ready) return + Log.d( + TAG, + "AACP session $generation ready; persistent configuration sent: " + + "listeningModes=$listeningModesSent, allowOffMode=$allowOffModeSent, " + + "stemConfig=$stemConfigSent" + ) + + sendBroadcast( + Intent(AirPodsNotifications.AIRPODS_CONNECTED).putExtra("device", device).apply { + setPackage(packageName) + } + ) + } + + private fun startAacpInitialization(generation: Long) { + val job = serviceScope.launch { + delay(200) + if (!isCurrentAacpGeneration(generation)) return@launch + aacpManager.sendPacket(aacpManager.createHandshakePacket()) + delay(200) + if (!isCurrentAacpGeneration(generation)) return@launch + aacpManager.sendSetFeatureFlagsPacket() + delay(200) + if (!isCurrentAacpGeneration(generation)) return@launch + aacpManager.sendNotificationRequest() + delay(200) + if (!isCurrentAacpGeneration(generation)) return@launch + aacpManager.sendSomePacketIDontKnowWhatItIs() + delay(200) + if (!isCurrentAacpGeneration(generation)) return@launch + aacpManager.sendRequestProximityKeys( + ( + AACPManager.Companion.ProximityKeyType.IRK.value + + AACPManager.Companion.ProximityKeyType.ENC_KEY.value + ).toByte() + ) + if (!handleIncomingCallOnceConnected) startHeadTracking() else handleIncomingCall() + delay(5000) + if (!isCurrentAacpGeneration(generation)) return@launch + aacpManager.sendPacket(aacpManager.createHandshakePacket()) + aacpManager.sendSetFeatureFlagsPacket() + aacpManager.sendNotificationRequest() + aacpManager.sendRequestProximityKeys( + AACPManager.Companion.ProximityKeyType.IRK.value + ) + if (!handleIncomingCallOnceConnected) stopHeadTracking() + } + synchronized(aacpSessionLock) { + if (aacpSessionGeneration == generation) { + aacpInitializationJob = job + } else { + job.cancel() + } + } + } + + private fun startAacpReader(socket: BluetoothSocket, generation: Long) { + val job = serviceScope.launch { + try { + while (socket.isConnected && isCurrentAacpGeneration(generation)) { + val buffer = ByteArray(1024) + val bytesRead = socket.inputStream.read(buffer) + if (bytesRead <= 0) { + if (bytesRead == -1) { + Log.d(TAG, "socket closed (bytesRead = -1)") + } + break + } + + val data = buffer.copyOfRange(0, bytesRead) + sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply { + putExtra("data", data) + setPackage(packageName) + }) + val formattedHex = data.joinToString(" ") { "%02X".format(it) } + updateNotificationContent( + true, + sharedPreferences.getString("name", device?.name), + batteryNotification.getBattery() + ) + aacpManager.receivePacket(data) + if (!isHeadTrackingData(data)) { + Log.d("AirPodsData", "Data received: $formattedHex") + logPacket(data, "AirPods") + } + } + } catch (e: Exception) { + if (isCurrentAacpGeneration(generation)) { + Log.w(TAG, "Error reading data, we have probably disconnected.", e) + } + } finally { + finishAacpSession(generation, socket) + } + } + synchronized(aacpSessionLock) { + if (aacpSessionGeneration == generation && + BluetoothConnectionManager.aacpSocket === socket + ) { + aacpReaderJob = job + } else { + job.cancel() + } + } + } + @SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag") fun connectToSocket( adapter: BluetoothAdapter, device: BluetoothDevice, manual: Boolean = false ) { - if (BluetoothConnectionManager.aacpSocket != null && BluetoothConnectionManager.aacpSocket?.isConnected == true) return + val generation = beginAacpConnection() ?: return Log.d(TAG, " Connecting to socket") - val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a") -// if (!isConnectedLocally) { + val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a") val socket = try { createBluetoothSocket(adapter, device, uuid, 4097) } catch (e: Exception) { Log.e(TAG, "Failed to create BluetoothSocket: ${e.message}") - showSocketConnectionFailureNotification("Failed to create Bluetooth socket: ${e.localizedMessage}") + if (isCurrentAacpGeneration(generation)) { + invalidateAacpSession() + } + showSocketConnectionFailureNotification( + "Failed to create Bluetooth socket: ${e.localizedMessage}" + ) return } + synchronized(aacpSessionLock) { + if (aacpSessionGeneration != generation || !connectionAttemptInProgress) { + runCatching { socket.close() } + return + } + pendingAacpSocket = socket + } + + var attSocket: BluetoothSocket? = null try { runBlocking { withTimeout(5000.milliseconds) { - try { - socket.connect() - this@AirPodsService.device = device - val xposedRemotePref = XposedRemotePrefProvider.create() - val attSocket = if (xposedRemotePref.getBoolean("vendor_id_hook", false)) { - createBluetoothSocket( - adapter, - device, - ParcelUuid.fromString("00000000-0000-0000-0000-000000000000"), - 31 - ) - } else null - attSocket?.connect() - - if (attSocket != null) { - attManager.startReader() - attManager.readCharacteristic(ATTHandles.LOUD_SOUND_REDUCTION) - attManager.readCharacteristic(ATTHandles.TRANSPARENCY) - attManager.readCharacteristic(ATTHandles.HEARING_AID) - } - - BluetoothConnectionManager.aacpSocket = socket - BluetoothConnectionManager.attSocket = attSocket - - // Create AirPodsInstance from stored config if available - if (airpodsInstance == null && config.airpodsModelNumber.isNotEmpty()) { - val model = - AirPodsModels.getModelByModelNumber(config.airpodsModelNumber) - if (model != null) { - airpodsInstance = AirPodsInstance( - name = config.airpodsName, - model = model, - actualModelNumber = config.airpodsModelNumber, - serialNumber = config.airpodsSerialNumber, - leftSerialNumber = config.airpodsLeftSerialNumber, - rightSerialNumber = config.airpodsRightSerialNumber, - version1 = config.airpodsVersion1, - version2 = config.airpodsVersion2, - version3 = config.airpodsVersion3, - ) - setMetadatas(device) - } - } - - updateNotificationContent( - true, config.deviceName, batteryNotification.getBattery() + socket.connect() + val xposedRemotePref = XposedRemotePrefProvider.create() + attSocket = if (xposedRemotePref.getBoolean("vendor_id_hook", false)) { + createBluetoothSocket( + adapter, + device, + ParcelUuid.fromString("00000000-0000-0000-0000-000000000000"), + 31 ) - Log.d(TAG, " Socket connected") - sharedPreferences.edit { putBoolean("connection_successful", true) } - if (!sharedPreferences.contains("first_connection_successful_time")) { - sharedPreferences.edit { - putLong( - "first_connection_successful_time", - System.currentTimeMillis() - ) - } - } - sendBroadcast(Intent(AirPodsNotifications.AIRPODS_L2CAP_CONNECTED)) - } catch (e: Exception) { -// sharedPreferences.edit { putBoolean("connection_successful", false) } - Log.d( - TAG, " Socket not connected, ${e.message}" - ) - if (manual) { - sendToast( - "Couldn't connect to socket: ${e.localizedMessage}" - ) - } else { - showSocketConnectionFailureNotification("Couldn't connect to socket: ${e.localizedMessage}") - } - return@withTimeout -// throw e // lol how did i not catch this before... gonna comment this line instead of removing to preserve history + } else { + null + } + attSocket?.connect() + + if (attSocket != null) { + attManager.startReader() + attManager.readCharacteristic(ATTHandles.LOUD_SOUND_REDUCTION) + attManager.readCharacteristic(ATTHandles.TRANSPARENCY) + attManager.readCharacteristic(ATTHandles.HEARING_AID) } } } - if (!socket.isConnected) { - Log.d(TAG, " socket not connected") - if (manual) { - sendToast( - "Couldn't connect to socket: timeout." - ) - } else { - showSocketConnectionFailureNotification("Couldn't connect to socket: Timeout") - } + + if (!socket.isConnected || !isCurrentAacpGeneration(generation) || + !activateAacpSession(generation, socket, attSocket) + ) { + runCatching { attSocket?.close() } + runCatching { socket.close() } return } - this@AirPodsService.device = device - BluetoothConnectionManager.aacpSocket?.let { - aacpManager.sendPacket(aacpManager.createHandshakePacket()) - aacpManager.sendSetFeatureFlagsPacket() - aacpManager.sendNotificationRequest() - Log.d(TAG, "Requesting proximity keys") - aacpManager.sendRequestProximityKeys((AACPManager.Companion.ProximityKeyType.IRK.value + AACPManager.Companion.ProximityKeyType.ENC_KEY.value).toByte()) - CoroutineScope(Dispatchers.IO).launch { - delay(200) - aacpManager.sendPacket(aacpManager.createHandshakePacket()) - delay(200) - aacpManager.sendSetFeatureFlagsPacket() - delay(200) - aacpManager.sendNotificationRequest() - delay(200) - aacpManager.sendSomePacketIDontKnowWhatItIs() - delay(200) - aacpManager.sendRequestProximityKeys((AACPManager.Companion.ProximityKeyType.IRK.value + AACPManager.Companion.ProximityKeyType.ENC_KEY.value).toByte()) - if (!handleIncomingCallOnceConnected) startHeadTracking() else handleIncomingCall() - Handler(Looper.getMainLooper()).postDelayed({ - aacpManager.sendPacket(aacpManager.createHandshakePacket()) - aacpManager.sendSetFeatureFlagsPacket() - aacpManager.sendNotificationRequest() - aacpManager.sendRequestProximityKeys(AACPManager.Companion.ProximityKeyType.IRK.value) - if (!handleIncomingCallOnceConnected) stopHeadTracking() - }, 5000) - - sendBroadcast( - Intent(AirPodsNotifications.AIRPODS_CONNECTED).putExtra("device", device) - .apply { - setPackage(packageName) - }) - - setupStemActions() - - while (socket.isConnected) { - try { - val buffer = ByteArray(1024) - val bytesRead = it.inputStream.read(buffer) - var data: ByteArray - if (bytesRead > 0) { - data = buffer.copyOfRange(0, bytesRead) - sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply { - putExtra("data", buffer.copyOfRange(0, bytesRead)) - setPackage(packageName) - }) - val bytes = buffer.copyOfRange(0, bytesRead) - val formattedHex = bytes.joinToString(" ") { "%02X".format(it) } -// CrossDevice.sendReceivedPacket(bytes) - updateNotificationContent( - true, - sharedPreferences.getString("name", device.name), - batteryNotification.getBattery() - ) - - aacpManager.receivePacket(data) - - if (!isHeadTrackingData(data)) { - Log.d("AirPodsData", "Data received: $formattedHex") - logPacket(data, "AirPods") - } - } else if (bytesRead == -1) { - Log.d("AirPodsService", "socket closed (bytesRead = -1)") - sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply { - setPackage(packageName) - }) - aacpManager.disconnected() - return@launch - } - } catch (e: Exception) { - Log.w(TAG, "Error reading data, we have probably disconnected.") - e.printStackTrace() - sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply { - setPackage(packageName) - }) - aacpManager.disconnected() - return@launch - } + this@AirPodsService.device = device + if (airpodsInstance == null && config.airpodsModelNumber.isNotEmpty()) { + val model = AirPodsModels.getModelByModelNumber(config.airpodsModelNumber) + if (model != null) { + airpodsInstance = AirPodsInstance( + name = config.airpodsName, + model = model, + actualModelNumber = config.airpodsModelNumber, + serialNumber = config.airpodsSerialNumber, + leftSerialNumber = config.airpodsLeftSerialNumber, + rightSerialNumber = config.airpodsRightSerialNumber, + version1 = config.airpodsVersion1, + version2 = config.airpodsVersion2, + version3 = config.airpodsVersion3, + ) + setMetadatas(device) + } + } - } - Log.d("AirPods Service", "socket closed") -// isConnectedLocally = false - aacpManager.disconnected() - updateNotificationContent(false) - sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply { - setPackage(packageName) - }) + updateNotificationContent(true, config.deviceName, batteryNotification.getBattery()) + Log.d(TAG, " Socket connected") + sharedPreferences.edit { putBoolean("connection_successful", true) } + if (!sharedPreferences.contains("first_connection_successful_time")) { + sharedPreferences.edit { + putLong("first_connection_successful_time", System.currentTimeMillis()) } } + + aacpManager.sendPacket(aacpManager.createHandshakePacket()) + aacpManager.sendSetFeatureFlagsPacket() + aacpManager.sendNotificationRequest() + Log.d(TAG, "Requesting proximity keys") + aacpManager.sendRequestProximityKeys( + ( + AACPManager.Companion.ProximityKeyType.IRK.value + + AACPManager.Companion.ProximityKeyType.ENC_KEY.value + ).toByte() + ) + startAacpReader(socket, generation) + startAacpInitialization(generation) } catch (e: Exception) { - e.printStackTrace() - Log.d(TAG, "Failed to connect to BluetoothConnectionManager.aacpSocket?: ${e.message}") - showSocketConnectionFailureNotification("Failed to establish connection: ${e.localizedMessage}") -// isConnectedLocally = false - this@AirPodsService.device = device - updateNotificationContent(false) + if (isCurrentAacpGeneration(generation)) { + invalidateAacpSession() + if (manual) { + sendToast("Couldn't connect to socket: ${e.localizedMessage}") + } else { + showSocketConnectionFailureNotification( + "Couldn't connect to socket: ${e.localizedMessage}" + ) + } + } else { + runCatching { attSocket?.close() } + runCatching { socket.close() } + } } -// } else { -// Log.d(TAG, "Already connected locally, skipping BluetoothConnectionManager.aacpSocket? connection (isConnectedLocally = $isConnectedLocally, BluetoothConnectionManager.aacpSocket?.isConnected = ${this::BluetoothConnectionManager.aacpSocket?.isInitialized && BluetoothConnectionManager.aacpSocket?.isConnected})") -// } } fun disconnectForCD() { - BluetoothConnectionManager.aacpSocket?.close() + invalidateAacpSession() MediaController.pausedWhileTakingOver = false Log.d(TAG, "Disconnected from AirPods, showing island.") showIsland( @@ -2873,17 +3076,13 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } fun disconnectAirPods() { - if (BluetoothConnectionManager.aacpSocket == null) return - try { - BluetoothConnectionManager.aacpSocket?.close() - } catch(e: Exception) { - Log.e(TAG, "error closing aacp socket ${e.message}") + val hasSession = synchronized(aacpSessionLock) { + connectionAttemptInProgress || + pendingAacpSocket != null || + BluetoothConnectionManager.aacpSocket != null } -// isConnectedLocally = false - aacpManager.disconnected() - - BluetoothConnectionManager.aacpSocket = null - BluetoothConnectionManager.attSocket = null + if (!hasSession) return + invalidateAacpSession() updateNotificationContent(false) sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply { @@ -3141,6 +3340,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } // isConnectedLocally = false // CrossDevice.isAvailable = true + serviceScope.cancel() + invalidateAacpSession() super.onDestroy() } From 4365e5c326a493de30a42e0310bcb83247e9f532 Mon Sep 17 00:00:00 2001 From: beeemT Date: Sat, 25 Jul 2026 17:03:19 +0200 Subject: [PATCH 2/5] Fix AACP session reconnect lifecycle --- .../librepods/services/AirPodsService.kt | 185 +++++++++++------- 1 file changed, 113 insertions(+), 72 deletions(-) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt index e945db979..65e1568b9 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt @@ -2679,6 +2679,33 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList aacpSessionGeneration == generation } } + private fun withCurrentAacpSession( + generation: Long, + socket: BluetoothSocket? = null, + block: () -> T + ): T? { + return synchronized(aacpSessionLock) { + val activeSocket = BluetoothConnectionManager.aacpSocket + if (aacpSessionGeneration != generation || + activeSocket == null || + (socket != null && activeSocket !== socket) + ) { + null + } else { + block() + } + } + } + private fun invalidateAacpSessionIfCurrent(generation: Long): Boolean { + return synchronized(aacpSessionLock) { + if (aacpSessionGeneration != generation) { + false + } else { + invalidateAacpSession() + true + } + } + } private fun activateAacpSession( generation: Long, @@ -2718,13 +2745,13 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList aacpInitializationJob = null BluetoothConnectionManager.aacpSocket = null BluetoothConnectionManager.attSocket = null + aacpManager.disconnected() } readerJob?.cancel() initializationJob?.cancel() sockets.forEach { socket -> runCatching { socket.close() } } - aacpManager.disconnected() } private fun finishAacpSession(generation: Long, socket: BluetoothSocket) { @@ -2748,6 +2775,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList aacpInitializationJob = null BluetoothConnectionManager.aacpSocket = null BluetoothConnectionManager.attSocket = null + aacpManager.disconnected() + updateNotificationContent(false) + sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply { + setPackage(packageName) + }) true } } @@ -2757,11 +2789,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList sockets.forEach { currentSocket -> runCatching { currentSocket.close() } } - aacpManager.disconnected() - updateNotificationContent(false) - sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply { - setPackage(packageName) - }) } private fun onAACPFrameReceived() { @@ -2813,6 +2840,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList BluetoothConnectionManager.aacpSocket != null ) { readyAacpSessionGeneration = generation + sendBroadcast( + Intent(AirPodsNotifications.AIRPODS_CONNECTED).putExtra("device", device).apply { + setPackage(packageName) + } + ) true } else { false @@ -2826,44 +2858,45 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList "stemConfig=$stemConfigSent" ) - sendBroadcast( - Intent(AirPodsNotifications.AIRPODS_CONNECTED).putExtra("device", device).apply { - setPackage(packageName) - } - ) } private fun startAacpInitialization(generation: Long) { val job = serviceScope.launch { delay(200) - if (!isCurrentAacpGeneration(generation)) return@launch - aacpManager.sendPacket(aacpManager.createHandshakePacket()) + if (withCurrentAacpSession(generation) { + aacpManager.sendPacket(aacpManager.createHandshakePacket()) + } == null) return@launch delay(200) - if (!isCurrentAacpGeneration(generation)) return@launch - aacpManager.sendSetFeatureFlagsPacket() + if (withCurrentAacpSession(generation) { + aacpManager.sendSetFeatureFlagsPacket() + } == null) return@launch delay(200) - if (!isCurrentAacpGeneration(generation)) return@launch - aacpManager.sendNotificationRequest() + if (withCurrentAacpSession(generation) { + aacpManager.sendNotificationRequest() + } == null) return@launch delay(200) - if (!isCurrentAacpGeneration(generation)) return@launch - aacpManager.sendSomePacketIDontKnowWhatItIs() + if (withCurrentAacpSession(generation) { + aacpManager.sendSomePacketIDontKnowWhatItIs() + } == null) return@launch delay(200) - if (!isCurrentAacpGeneration(generation)) return@launch - aacpManager.sendRequestProximityKeys( - ( - AACPManager.Companion.ProximityKeyType.IRK.value + - AACPManager.Companion.ProximityKeyType.ENC_KEY.value - ).toByte() - ) + if (withCurrentAacpSession(generation) { + aacpManager.sendRequestProximityKeys( + ( + AACPManager.Companion.ProximityKeyType.IRK.value + + AACPManager.Companion.ProximityKeyType.ENC_KEY.value + ).toByte() + ) + } == null) return@launch if (!handleIncomingCallOnceConnected) startHeadTracking() else handleIncomingCall() delay(5000) - if (!isCurrentAacpGeneration(generation)) return@launch - aacpManager.sendPacket(aacpManager.createHandshakePacket()) - aacpManager.sendSetFeatureFlagsPacket() - aacpManager.sendNotificationRequest() - aacpManager.sendRequestProximityKeys( - AACPManager.Companion.ProximityKeyType.IRK.value - ) + if (withCurrentAacpSession(generation) { + aacpManager.sendPacket(aacpManager.createHandshakePacket()) + aacpManager.sendSetFeatureFlagsPacket() + aacpManager.sendNotificationRequest() + aacpManager.sendRequestProximityKeys( + AACPManager.Companion.ProximityKeyType.IRK.value + ) + } == null) return@launch if (!handleIncomingCallOnceConnected) stopHeadTracking() } synchronized(aacpSessionLock) { @@ -2878,7 +2911,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList private fun startAacpReader(socket: BluetoothSocket, generation: Long) { val job = serviceScope.launch { try { - while (socket.isConnected && isCurrentAacpGeneration(generation)) { + while (withCurrentAacpSession(generation, socket) { socket.isConnected } == true) { val buffer = ByteArray(1024) val bytesRead = socket.inputStream.read(buffer) if (bytesRead <= 0) { @@ -2888,22 +2921,24 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList break } - val data = buffer.copyOfRange(0, bytesRead) - sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply { - putExtra("data", data) - setPackage(packageName) - }) - val formattedHex = data.joinToString(" ") { "%02X".format(it) } - updateNotificationContent( - true, - sharedPreferences.getString("name", device?.name), - batteryNotification.getBattery() - ) - aacpManager.receivePacket(data) - if (!isHeadTrackingData(data)) { - Log.d("AirPodsData", "Data received: $formattedHex") - logPacket(data, "AirPods") - } + if (withCurrentAacpSession(generation, socket) { + val data = buffer.copyOfRange(0, bytesRead) + sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply { + putExtra("data", data) + setPackage(packageName) + }) + val formattedHex = data.joinToString(" ") { "%02X".format(it) } + updateNotificationContent( + true, + sharedPreferences.getString("name", device?.name), + batteryNotification.getBattery() + ) + aacpManager.receivePacket(data) + if (!isHeadTrackingData(data)) { + Log.d("AirPodsData", "Data received: $formattedHex") + logPacket(data, "AirPods") + } + } == null) break } } catch (e: Exception) { if (isCurrentAacpGeneration(generation)) { @@ -2935,12 +2970,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList createBluetoothSocket(adapter, device, uuid, 4097) } catch (e: Exception) { Log.e(TAG, "Failed to create BluetoothSocket: ${e.message}") - if (isCurrentAacpGeneration(generation)) { - invalidateAacpSession() + if (invalidateAacpSessionIfCurrent(generation)) { + showSocketConnectionFailureNotification( + "Failed to create Bluetooth socket: ${e.localizedMessage}" + ) } - showSocketConnectionFailureNotification( - "Failed to create Bluetooth socket: ${e.localizedMessage}" - ) return } @@ -2979,11 +3013,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } } - if (!socket.isConnected || !isCurrentAacpGeneration(generation) || - !activateAacpSession(generation, socket, attSocket) - ) { + val activated = socket.isConnected && + isCurrentAacpGeneration(generation) && + activateAacpSession(generation, socket, attSocket) + if (!activated) { runCatching { attSocket?.close() } - runCatching { socket.close() } + if (!invalidateAacpSessionIfCurrent(generation)) { + runCatching { socket.close() } + } return } @@ -3015,21 +3052,25 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } } - aacpManager.sendPacket(aacpManager.createHandshakePacket()) - aacpManager.sendSetFeatureFlagsPacket() - aacpManager.sendNotificationRequest() - Log.d(TAG, "Requesting proximity keys") - aacpManager.sendRequestProximityKeys( - ( - AACPManager.Companion.ProximityKeyType.IRK.value + - AACPManager.Companion.ProximityKeyType.ENC_KEY.value - ).toByte() - ) + if (withCurrentAacpSession(generation) { + aacpManager.sendPacket(aacpManager.createHandshakePacket()) + aacpManager.sendSetFeatureFlagsPacket() + aacpManager.sendNotificationRequest() + Log.d(TAG, "Requesting proximity keys") + aacpManager.sendRequestProximityKeys( + ( + AACPManager.Companion.ProximityKeyType.IRK.value + + AACPManager.Companion.ProximityKeyType.ENC_KEY.value + ).toByte() + ) + } == null) { + return + } startAacpReader(socket, generation) startAacpInitialization(generation) } catch (e: Exception) { - if (isCurrentAacpGeneration(generation)) { - invalidateAacpSession() + runCatching { attSocket?.close() } + if (invalidateAacpSessionIfCurrent(generation)) { if (manual) { sendToast("Couldn't connect to socket: ${e.localizedMessage}") } else { @@ -3038,12 +3079,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList ) } } else { - runCatching { attSocket?.close() } runCatching { socket.close() } } } } + fun disconnectForCD() { invalidateAacpSession() MediaController.pausedWhileTakingOver = false From 6ee132146e9b18391d1c643fb21735e4dd1aecd6 Mon Sep 17 00:00:00 2001 From: beeemT Date: Sat, 25 Jul 2026 17:23:20 +0200 Subject: [PATCH 3/5] Simplify AACP session guards --- .../librepods/services/AirPodsService.kt | 117 ++++++++++-------- 1 file changed, 66 insertions(+), 51 deletions(-) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt index 65e1568b9..aeb1bbf45 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt @@ -2679,29 +2679,33 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList aacpSessionGeneration == generation } } - private fun withCurrentAacpSession( + + private fun isCurrentAacpSession( + generation: Long, + socket: BluetoothSocket? = null + ): Boolean { + return synchronized(aacpSessionLock) { + val activeSocket = BluetoothConnectionManager.aacpSocket + aacpSessionGeneration == generation && + activeSocket != null && + (socket == null || activeSocket === socket) + } + } + + private fun runWithCurrentAacpSession( generation: Long, socket: BluetoothSocket? = null, - block: () -> T - ): T? { + block: () -> Unit + ): Boolean { return synchronized(aacpSessionLock) { val activeSocket = BluetoothConnectionManager.aacpSocket if (aacpSessionGeneration != generation || activeSocket == null || (socket != null && activeSocket !== socket) ) { - null - } else { - block() - } - } - } - private fun invalidateAacpSessionIfCurrent(generation: Long): Boolean { - return synchronized(aacpSessionLock) { - if (aacpSessionGeneration != generation) { false } else { - invalidateAacpSession() + block() true } } @@ -2725,35 +2729,46 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } } - private fun invalidateAacpSession() { + private fun invalidateAacpSession(expectedGeneration: Long? = null): Boolean { var sockets: List = emptyList() var readerJob: Job? = null var initializationJob: Job? = null - synchronized(aacpSessionLock) { - aacpSessionGeneration += 1 - connectionAttemptInProgress = false - readyAacpSessionGeneration = null - readerJob = aacpReaderJob - initializationJob = aacpInitializationJob - sockets = listOfNotNull( - pendingAacpSocket, - BluetoothConnectionManager.aacpSocket, - BluetoothConnectionManager.attSocket - ).distinct() - pendingAacpSocket = null - aacpReaderJob = null - aacpInitializationJob = null - BluetoothConnectionManager.aacpSocket = null - BluetoothConnectionManager.attSocket = null - aacpManager.disconnected() + val invalidated = synchronized(aacpSessionLock) { + if (expectedGeneration != null && + aacpSessionGeneration != expectedGeneration + ) { + false + } else { + aacpSessionGeneration += 1 + connectionAttemptInProgress = false + readyAacpSessionGeneration = null + readerJob = aacpReaderJob + initializationJob = aacpInitializationJob + sockets = listOfNotNull( + pendingAacpSocket, + BluetoothConnectionManager.aacpSocket, + BluetoothConnectionManager.attSocket + ).distinct() + pendingAacpSocket = null + aacpReaderJob = null + aacpInitializationJob = null + BluetoothConnectionManager.aacpSocket = null + BluetoothConnectionManager.attSocket = null + aacpManager.disconnected() + true + } } + if (!invalidated) return false + readerJob?.cancel() initializationJob?.cancel() sockets.forEach { socket -> runCatching { socket.close() } } + return true } + private fun finishAacpSession(generation: Long, socket: BluetoothSocket) { var sockets: List = emptyList() var initializationJob: Job? = null @@ -2863,40 +2878,40 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList private fun startAacpInitialization(generation: Long) { val job = serviceScope.launch { delay(200) - if (withCurrentAacpSession(generation) { + if (!runWithCurrentAacpSession(generation) { aacpManager.sendPacket(aacpManager.createHandshakePacket()) - } == null) return@launch + }) return@launch delay(200) - if (withCurrentAacpSession(generation) { + if (!runWithCurrentAacpSession(generation) { aacpManager.sendSetFeatureFlagsPacket() - } == null) return@launch + }) return@launch delay(200) - if (withCurrentAacpSession(generation) { + if (!runWithCurrentAacpSession(generation) { aacpManager.sendNotificationRequest() - } == null) return@launch + }) return@launch delay(200) - if (withCurrentAacpSession(generation) { + if (!runWithCurrentAacpSession(generation) { aacpManager.sendSomePacketIDontKnowWhatItIs() - } == null) return@launch + }) return@launch delay(200) - if (withCurrentAacpSession(generation) { + if (!runWithCurrentAacpSession(generation) { aacpManager.sendRequestProximityKeys( ( AACPManager.Companion.ProximityKeyType.IRK.value + AACPManager.Companion.ProximityKeyType.ENC_KEY.value ).toByte() ) - } == null) return@launch + }) return@launch if (!handleIncomingCallOnceConnected) startHeadTracking() else handleIncomingCall() delay(5000) - if (withCurrentAacpSession(generation) { + if (!runWithCurrentAacpSession(generation) { aacpManager.sendPacket(aacpManager.createHandshakePacket()) aacpManager.sendSetFeatureFlagsPacket() aacpManager.sendNotificationRequest() aacpManager.sendRequestProximityKeys( AACPManager.Companion.ProximityKeyType.IRK.value ) - } == null) return@launch + }) return@launch if (!handleIncomingCallOnceConnected) stopHeadTracking() } synchronized(aacpSessionLock) { @@ -2911,7 +2926,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList private fun startAacpReader(socket: BluetoothSocket, generation: Long) { val job = serviceScope.launch { try { - while (withCurrentAacpSession(generation, socket) { socket.isConnected } == true) { + while (isCurrentAacpSession(generation, socket) && socket.isConnected) { val buffer = ByteArray(1024) val bytesRead = socket.inputStream.read(buffer) if (bytesRead <= 0) { @@ -2921,7 +2936,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList break } - if (withCurrentAacpSession(generation, socket) { + if (!runWithCurrentAacpSession(generation, socket) { val data = buffer.copyOfRange(0, bytesRead) sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply { putExtra("data", data) @@ -2938,7 +2953,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList Log.d("AirPodsData", "Data received: $formattedHex") logPacket(data, "AirPods") } - } == null) break + }) break } } catch (e: Exception) { if (isCurrentAacpGeneration(generation)) { @@ -2970,7 +2985,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList createBluetoothSocket(adapter, device, uuid, 4097) } catch (e: Exception) { Log.e(TAG, "Failed to create BluetoothSocket: ${e.message}") - if (invalidateAacpSessionIfCurrent(generation)) { + if (invalidateAacpSession(generation)) { showSocketConnectionFailureNotification( "Failed to create Bluetooth socket: ${e.localizedMessage}" ) @@ -3018,7 +3033,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList activateAacpSession(generation, socket, attSocket) if (!activated) { runCatching { attSocket?.close() } - if (!invalidateAacpSessionIfCurrent(generation)) { + if (!invalidateAacpSession(generation)) { runCatching { socket.close() } } return @@ -3052,7 +3067,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } } - if (withCurrentAacpSession(generation) { + if (!runWithCurrentAacpSession(generation) { aacpManager.sendPacket(aacpManager.createHandshakePacket()) aacpManager.sendSetFeatureFlagsPacket() aacpManager.sendNotificationRequest() @@ -3063,14 +3078,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList AACPManager.Companion.ProximityKeyType.ENC_KEY.value ).toByte() ) - } == null) { + }) { return } startAacpReader(socket, generation) startAacpInitialization(generation) } catch (e: Exception) { runCatching { attSocket?.close() } - if (invalidateAacpSessionIfCurrent(generation)) { + if (invalidateAacpSession(generation)) { if (manual) { sendToast("Couldn't connect to socket: ${e.localizedMessage}") } else { From 9b27612192bc2cb5669bdf358760cded042109db Mon Sep 17 00:00:00 2001 From: beeemT Date: Sat, 25 Jul 2026 17:31:07 +0200 Subject: [PATCH 4/5] Guard AACP initialization side effects --- .../me/kavishdevar/librepods/services/AirPodsService.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt index aeb1bbf45..20061098d 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt @@ -2902,7 +2902,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList ).toByte() ) }) return@launch - if (!handleIncomingCallOnceConnected) startHeadTracking() else handleIncomingCall() + if (!runWithCurrentAacpSession(generation) { + if (!handleIncomingCallOnceConnected) startHeadTracking() else handleIncomingCall() + }) return@launch delay(5000) if (!runWithCurrentAacpSession(generation) { aacpManager.sendPacket(aacpManager.createHandshakePacket()) @@ -2912,7 +2914,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList AACPManager.Companion.ProximityKeyType.IRK.value ) }) return@launch - if (!handleIncomingCallOnceConnected) stopHeadTracking() + if (!runWithCurrentAacpSession(generation) { + if (!handleIncomingCallOnceConnected) stopHeadTracking() + }) return@launch } synchronized(aacpSessionLock) { if (aacpSessionGeneration == generation) { From b58d090b08a22265edf754b19db0361f62622aae Mon Sep 17 00:00:00 2001 From: beeemT Date: Sat, 25 Jul 2026 17:34:25 +0200 Subject: [PATCH 5/5] Reset AACP state before reconnect --- .../java/me/kavishdevar/librepods/services/AirPodsService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt index 20061098d..f9e14f3a5 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt @@ -2665,6 +2665,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList ).distinct() BluetoothConnectionManager.aacpSocket = null BluetoothConnectionManager.attSocket = null + aacpManager.disconnected() pendingAacpSocket = null aacpSessionGeneration }