Skip to content

Commit bc5a12b

Browse files
committed
fix(android): prevent L2CAP socket teardown during AACP handshake
The socketActuallyAlive check required aacpManager.connectedDevices to be non-empty, but during the AACP handshake window (~seconds after socket.connect() succeeds) this list is still empty. A second connectToSocket call from the A2DP profile proxy callback would see the socket as "stale", tear it down, and fail to reconnect. - Add 10-second handshake grace period to socketActuallyAlive check - Remove premature isConnectedLocally=true from connectionReceiver and takeOver (connectToSocket sets it internally on success) - Wrap socket read loop in try/catch/finally to properly handle IOException from remote disconnect, preventing stale socket state
1 parent 6211c4c commit bc5a12b

File tree

1 file changed

+42
-41
lines changed

1 file changed

+42
-41
lines changed

android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
671671
}
672672
Log.d(TAG, "Setting metadata")
673673
setMetadatas(device!!)
674-
isConnectedLocally = true
675674
macAddress = device!!.address
676675
sharedPreferences.edit {
677676
putString("mac_address", macAddress)
@@ -1463,6 +1462,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
14631462

14641463
var isConnectedLocally = false
14651464
private val isConnecting = java.util.concurrent.atomic.AtomicBoolean(false)
1465+
@Volatile private var socketConnectedAt: Long = 0
14661466
var device: BluetoothDevice? = null
14671467

14681468
private lateinit var earReceiver: BroadcastReceiver
@@ -2372,7 +2372,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
23722372
} else {
23732373
connectToSocket(device!!)
23742374
connectAudio(this, device)
2375-
isConnectedLocally = true
23762375
}
23772376
}
23782377
showIsland(this, batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level!!.coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level!!),
@@ -2427,8 +2426,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
24272426
Log.d(TAG, "<LogCollector:Start> Connecting to socket")
24282427
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
24292428
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
2429+
val inHandshakeWindow = System.currentTimeMillis() - socketConnectedAt < 10_000
24302430
val socketActuallyAlive = isConnectedLocally && this::socket.isInitialized &&
2431-
socket.isConnected && aacpManager.connectedDevices.isNotEmpty()
2431+
socket.isConnected && (aacpManager.connectedDevices.isNotEmpty() || inHandshakeWindow)
24322432
if (!socketActuallyAlive) {
24332433
if (isConnectedLocally) {
24342434
Log.d(TAG, "isConnectedLocally was true but socket is dead, resetting")
@@ -2450,6 +2450,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
24502450
try {
24512451
socket.connect()
24522452
isConnectedLocally = true
2453+
socketConnectedAt = System.currentTimeMillis()
24532454
this@AirPodsService.device = device
24542455

24552456
BluetoothConnectionManager.setCurrentConnection(socket, device)
@@ -2544,50 +2545,50 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
25442545

25452546
setupStemActions()
25462547

2547-
while (socket.isConnected) {
2548-
socket.let { it ->
2549-
val buffer = ByteArray(1024)
2550-
val bytesRead = it.inputStream.read(buffer)
2551-
var data: ByteArray
2552-
if (bytesRead > 0) {
2553-
data = buffer.copyOfRange(0, bytesRead)
2554-
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply {
2555-
putExtra("data", buffer.copyOfRange(0, bytesRead))
2556-
})
2557-
val bytes = buffer.copyOfRange(0, bytesRead)
2558-
val formattedHex = bytes.joinToString(" ") { "%02X".format(it) }
2559-
// CrossDevice.sendReceivedPacket(bytes)
2560-
updateNotificationContent(
2561-
true,
2562-
sharedPreferences.getString("name", device.name),
2563-
batteryNotification.getBattery()
2564-
)
2548+
try {
2549+
while (socket.isConnected) {
2550+
socket.let { it ->
2551+
val buffer = ByteArray(1024)
2552+
val bytesRead = it.inputStream.read(buffer)
2553+
var data: ByteArray
2554+
if (bytesRead > 0) {
2555+
data = buffer.copyOfRange(0, bytesRead)
2556+
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply {
2557+
putExtra("data", buffer.copyOfRange(0, bytesRead))
2558+
})
2559+
val bytes = buffer.copyOfRange(0, bytesRead)
2560+
val formattedHex = bytes.joinToString(" ") { "%02X".format(it) }
2561+
// CrossDevice.sendReceivedPacket(bytes)
2562+
updateNotificationContent(
2563+
true,
2564+
sharedPreferences.getString("name", device.name),
2565+
batteryNotification.getBattery()
2566+
)
25652567

2566-
aacpManager.receivePacket(data)
2568+
aacpManager.receivePacket(data)
25672569

2568-
if (!isHeadTrackingData(data)) {
2569-
Log.d("AirPodsData", "Data received: $formattedHex")
2570-
logPacket(data, "AirPods")
2571-
}
2570+
if (!isHeadTrackingData(data)) {
2571+
Log.d("AirPodsData", "Data received: $formattedHex")
2572+
logPacket(data, "AirPods")
2573+
}
25722574

2573-
} else if (bytesRead == -1) {
2574-
Log.d("AirPods Service", "Socket closed (bytesRead = -1)")
2575-
isConnectedLocally = false
2576-
isConnecting.set(false)
2577-
socket.close()
2578-
aacpManager.disconnected()
2579-
updateNotificationContent(false)
2580-
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
2581-
return@launch
2575+
} else if (bytesRead == -1) {
2576+
Log.d("AirPods Service", "Socket closed (bytesRead = -1)")
2577+
break
2578+
}
25822579
}
25832580
}
2581+
Log.d("AirPods Service", "Socket closed")
2582+
} catch (e: java.io.IOException) {
2583+
Log.d("AirPods Service", "Socket read error: ${e.message}")
2584+
} finally {
2585+
isConnectedLocally = false
2586+
isConnecting.set(false)
2587+
try { socket.close() } catch (_: Exception) {}
2588+
aacpManager.disconnected()
2589+
updateNotificationContent(false)
2590+
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
25842591
}
2585-
Log.d("AirPods Service", "Socket closed")
2586-
isConnectedLocally = false
2587-
socket.close()
2588-
aacpManager.disconnected()
2589-
updateNotificationContent(false)
2590-
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
25912592
}
25922593
}
25932594
} catch (e: Exception) {

0 commit comments

Comments
 (0)