Skip to content

Commit 5bf4e00

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 5bf4e00

1 file changed

Lines changed: 39 additions & 10 deletions

File tree

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

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2360,6 +2360,13 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
23602360

23612361
@Suppress("ClassName")
23622362
private object bluetoothReceiver : BroadcastReceiver() {
2363+
private fun sendDetected(context: Context?, name: String?, device: BluetoothDevice) {
2364+
val intent = Intent(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
2365+
intent.putExtra("name", name)
2366+
intent.putExtra("device", device)
2367+
context?.sendBroadcast(intent)
2368+
}
2369+
23632370
@SuppressLint("MissingPermission")
23642371
override fun onReceive(context: Context?, intent: Intent) {
23652372
val bluetoothDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -2375,16 +2382,20 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
23752382
?.getString("name", bluetoothDevice?.name)
23762383
if (bluetoothDevice != null && !action.isNullOrEmpty()) {
23772384
Log.d(TAG, "Received bluetooth connection broadcast: action=$action")
2385+
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
23782386
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-
}
2387+
if (bluetoothDevice.uuids?.contains(uuid) == true) {
2388+
sendDetected(context, name, bluetoothDevice)
2389+
} else {
2390+
bluetoothDevice.fetchUuidsWithSdp()
2391+
}
2392+
} else if ("android.bluetooth.device.action.UUID" == action) {
2393+
val savedMac = context?.getSharedPreferences("settings", MODE_PRIVATE)
2394+
?.getString("mac_address", "") ?: ""
2395+
val matchedByMac = savedMac.isNotEmpty() && bluetoothDevice.address == savedMac
2396+
val matchedByUuid = bluetoothDevice.uuids?.contains(uuid) == true
2397+
if (matchedByUuid || matchedByMac) {
2398+
sendDetected(context, name, bluetoothDevice)
23882399
}
23892400
}
23902401
}
@@ -2642,7 +2653,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
26422653

26432654
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
26442655
fun connectToSocket(
2645-
adapter: BluetoothAdapter, device: BluetoothDevice, manual: Boolean = false
2656+
adapter: BluetoothAdapter, device: BluetoothDevice, manual: Boolean = false, retriesLeft: Int = if (manual) 0 else 3
26462657
) {
26472658
Log.d(TAG, "<LogCollector:Start> Connecting to socket")
26482659
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
@@ -2701,6 +2712,15 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
27012712
Log.d(
27022713
TAG, "<LogCollector:Complete:Failed> Socket not connected, ${e.message}"
27032714
)
2715+
if (retriesLeft > 0) {
2716+
Log.d(TAG, "Retrying socket connect, $retriesLeft attempts left")
2717+
try { socket.close() } catch (_: Exception) {}
2718+
CoroutineScope(Dispatchers.IO).launch {
2719+
delay(500L)
2720+
connectToSocket(adapter, device, manual, retriesLeft - 1)
2721+
}
2722+
return@withTimeout
2723+
}
27042724
if (manual) {
27052725
sendToast(
27062726
"Couldn't connect to socket: ${e.localizedMessage}"
@@ -2715,6 +2735,15 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
27152735
}
27162736
if (!socket.isConnected) {
27172737
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected")
2738+
if (retriesLeft > 0) {
2739+
Log.d(TAG, "Retrying socket connect after timeout, $retriesLeft attempts left")
2740+
try { socket.close() } catch (_: Exception) {}
2741+
CoroutineScope(Dispatchers.IO).launch {
2742+
delay(500L)
2743+
connectToSocket(adapter, device, manual, retriesLeft - 1)
2744+
}
2745+
return
2746+
}
27182747
if (manual) {
27192748
sendToast(
27202749
"Couldn't connect to socket: timeout."

0 commit comments

Comments
 (0)