Skip to content

Commit 7ec7741

Browse files
committed
android: detect auto-reconnected AirPods via ACTION_UUID and retry L2CAP socket connect
ACL_CONNECTED only kicks off SDP via fetchUuidsWithSdp; the actual detection broadcast fires from the ACTION_UUID handler, which the system delivers once SDP completes. That is the correct ready-signal instead of polling the cached UUIDs immediately. connectToSocket now retries up to three times with 500 ms backoff when called from the auto-detect path. The L2CAP server on the AirPods sometimes is not ready the first instant the ACL link comes up (also seen when force-connecting from Bluetooth settings while the AirPods were owned by another device). Manual reconnects keep the original behaviour and show the error toast on first failure. Closes #569
1 parent fb44f01 commit 7ec7741

1 file changed

Lines changed: 44 additions & 10 deletions

File tree

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

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
650650
connectionReceiver = object : BroadcastReceiver() {
651651
override fun onReceive(context: Context?, intent: Intent?) {
652652
if (intent?.action == AirPodsNotifications.AIRPODS_CONNECTION_DETECTED) {
653+
if (::socket.isInitialized && socket.isConnected) {
654+
Log.d(TAG, "Connection broadcast received but socket already connected, ignoring")
655+
return
656+
}
657+
653658
device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
654659
intent.getParcelableExtra("device", BluetoothDevice::class.java)!!
655660
} else {
@@ -2360,6 +2365,13 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
23602365

23612366
@Suppress("ClassName")
23622367
private object bluetoothReceiver : BroadcastReceiver() {
2368+
private fun sendDetected(context: Context?, name: String?, device: BluetoothDevice) {
2369+
val intent = Intent(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
2370+
intent.putExtra("name", name)
2371+
intent.putExtra("device", device)
2372+
context?.sendBroadcast(intent)
2373+
}
2374+
23632375
@SuppressLint("MissingPermission")
23642376
override fun onReceive(context: Context?, intent: Intent) {
23652377
val bluetoothDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -2375,16 +2387,20 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
23752387
?.getString("name", bluetoothDevice?.name)
23762388
if (bluetoothDevice != null && !action.isNullOrEmpty()) {
23772389
Log.d(TAG, "Received bluetooth connection broadcast: action=$action")
2390+
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
23782391
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
2379-
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
2380-
bluetoothDevice.fetchUuidsWithSdp()
2381-
if (bluetoothDevice.uuids != null) {
2382-
if (bluetoothDevice.uuids.contains(uuid)) {
2383-
val intent = Intent(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
2384-
intent.putExtra("name", name)
2385-
intent.putExtra("device", bluetoothDevice)
2386-
context?.sendBroadcast(intent)
2387-
}
2392+
if (bluetoothDevice.uuids?.contains(uuid) == true) {
2393+
sendDetected(context, name, bluetoothDevice)
2394+
} else {
2395+
bluetoothDevice.fetchUuidsWithSdp()
2396+
}
2397+
} else if ("android.bluetooth.device.action.UUID" == action) {
2398+
val savedMac = context?.getSharedPreferences("settings", MODE_PRIVATE)
2399+
?.getString("mac_address", "") ?: ""
2400+
val matchedByMac = savedMac.isNotEmpty() && bluetoothDevice.address == savedMac
2401+
val matchedByUuid = bluetoothDevice.uuids?.contains(uuid) == true
2402+
if (matchedByUuid || matchedByMac) {
2403+
sendDetected(context, name, bluetoothDevice)
23882404
}
23892405
}
23902406
}
@@ -2642,7 +2658,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
26422658

26432659
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
26442660
fun connectToSocket(
2645-
adapter: BluetoothAdapter, device: BluetoothDevice, manual: Boolean = false
2661+
adapter: BluetoothAdapter, device: BluetoothDevice, manual: Boolean = false, retriesLeft: Int = if (manual) 0 else 3
26462662
) {
26472663
Log.d(TAG, "<LogCollector:Start> Connecting to socket")
26482664
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
@@ -2701,6 +2717,15 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
27012717
Log.d(
27022718
TAG, "<LogCollector:Complete:Failed> Socket not connected, ${e.message}"
27032719
)
2720+
if (retriesLeft > 0) {
2721+
Log.d(TAG, "Retrying socket connect, $retriesLeft attempts left")
2722+
try { socket.close() } catch (_: Exception) {}
2723+
CoroutineScope(Dispatchers.IO).launch {
2724+
delay(500L)
2725+
connectToSocket(adapter, device, manual, retriesLeft - 1)
2726+
}
2727+
return@withTimeout
2728+
}
27042729
if (manual) {
27052730
sendToast(
27062731
"Couldn't connect to socket: ${e.localizedMessage}"
@@ -2715,6 +2740,15 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
27152740
}
27162741
if (!socket.isConnected) {
27172742
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected")
2743+
if (retriesLeft > 0) {
2744+
Log.d(TAG, "Retrying socket connect after timeout, $retriesLeft attempts left")
2745+
try { socket.close() } catch (_: Exception) {}
2746+
CoroutineScope(Dispatchers.IO).launch {
2747+
delay(500L)
2748+
connectToSocket(adapter, device, manual, retriesLeft - 1)
2749+
}
2750+
return
2751+
}
27182752
if (manual) {
27192753
sendToast(
27202754
"Couldn't connect to socket: timeout."

0 commit comments

Comments
 (0)