android: listen to UUID broadcasts

This commit is contained in:
Kavish Devar
2026-05-29 18:25:58 +05:30
parent 3c3c0edffd
commit 571db0ebde
3 changed files with 95 additions and 34 deletions

View File

@@ -52,13 +52,13 @@ enum class ATTCCCDHandles(val value: Int) {
class ATTManager(private val adapter: BluetoothAdapter, private val device: BluetoothDevice) { class ATTManager(private val adapter: BluetoothAdapter, private val device: BluetoothDevice) {
companion object { companion object {
private const val TAG = "ATTManager"
private const val OPCODE_READ_REQUEST: Byte = 0x0A private const val OPCODE_READ_REQUEST: Byte = 0x0A
private const val OPCODE_WRITE_REQUEST: Byte = 0x12 private const val OPCODE_WRITE_REQUEST: Byte = 0x12
private const val OPCODE_HANDLE_VALUE_NTF: Byte = 0x1B private const val OPCODE_HANDLE_VALUE_NTF: Byte = 0x1B
} }
private val id = System.identityHashCode(this)
@Suppress("PrivatePropertyName")
private val TAG = "ATTManager[$id]"
var socket: BluetoothSocket? = null var socket: BluetoothSocket? = null
private var input: InputStream? = null private var input: InputStream? = null
private var output: OutputStream? = null private var output: OutputStream? = null
@@ -72,16 +72,25 @@ class ATTManager(private val adapter: BluetoothAdapter, private val device: Blue
fun connect() { fun connect() {
val uuid = ParcelUuid.fromString("00000000-0000-0000-0000-000000000000") val uuid = ParcelUuid.fromString("00000000-0000-0000-0000-000000000000")
try { if (socket == null) {
socket = createBluetoothSocket(adapter, device, uuid) Log.d(TAG, "Socket doesn't exist, creating")
} catch (e: Exception) { try {
Log.w(TAG, "Failed to create socket") socket = createBluetoothSocket(adapter, device, uuid)
} catch (e: Exception) {
Log.w(TAG, "Failed to create socket")
e.printStackTrace()
return
}
} }
try { if (socket?.isConnected != true) {
socket!!.connect() Log.d(TAG, "Connection to socket")
} catch (e: Exception) { try {
Log.w(TAG, "ATT socket failed to connect") socket!!.connect()
return } catch (e: Exception) {
Log.w(TAG, "ATT socket failed to connect")
e.printStackTrace()
return
}
} }
input = socket!!.inputStream input = socket!!.inputStream
output = socket!!.outputStream output = socket!!.outputStream
@@ -113,6 +122,10 @@ class ATTManager(private val adapter: BluetoothAdapter, private val device: Blue
} }
} }
} }
if (output != null) {
Log.d(TAG, "sending read req for hearing aid declaration")
output?.write(byteArrayOf(0x0A, 0x29, 0x00))
}
} }
fun disconnect() { fun disconnect() {
@@ -216,11 +229,11 @@ class ATTManager(private val adapter: BluetoothAdapter, private val device: Blue
) )
val constructors = BluetoothSocket::class.java.declaredConstructors val constructors = BluetoothSocket::class.java.declaredConstructors
Log.d("ATTManager", "BluetoothSocket has ${constructors.size} constructors:") Log.d(TAG, "BluetoothSocket has ${constructors.size} constructors:")
constructors.forEachIndexed { index, constructor -> constructors.forEachIndexed { index, constructor ->
val params = constructor.parameterTypes.joinToString(", ") { it.simpleName } val params = constructor.parameterTypes.joinToString(", ") { it.simpleName }
Log.d("ATTManager", "Constructor $index: ($params)") Log.d(TAG, "Constructor $index: ($params)")
} }
var lastException: Exception? = null var lastException: Exception? = null
@@ -228,7 +241,7 @@ class ATTManager(private val adapter: BluetoothAdapter, private val device: Blue
for ((index, params) in constructorSpecs.withIndex()) { for ((index, params) in constructorSpecs.withIndex()) {
try { try {
Log.d("ATTManager", "Trying constructor signature #${index + 1}") Log.d(TAG, "Trying constructor signature #${index + 1}")
attemptedConstructors++ attemptedConstructors++
val paramTypes = params.map { it::class.javaPrimitiveType ?: it::class.java }.toTypedArray() val paramTypes = params.map { it::class.javaPrimitiveType ?: it::class.java }.toTypedArray()
@@ -237,13 +250,13 @@ class ATTManager(private val adapter: BluetoothAdapter, private val device: Blue
return constructor.newInstance(*params) as BluetoothSocket return constructor.newInstance(*params) as BluetoothSocket
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ATTManager", "Constructor signature #${index + 1} failed: ${e.message}") Log.e(TAG, "Constructor signature #${index + 1} failed: ${e.message}")
lastException = e lastException = e
} }
} }
val errorMessage = "Failed to create BluetoothSocket after trying $attemptedConstructors constructor signatures" val errorMessage = "Failed to create BluetoothSocket after trying $attemptedConstructors constructor signatures"
Log.e("ATTManager", errorMessage) Log.e(TAG, errorMessage)
throw lastException ?: IllegalStateException(errorMessage) throw lastException ?: IllegalStateException(errorMessage)
} }
} }

View File

@@ -530,7 +530,9 @@ class AirPodsViewModel(
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
service.attManager?.connect() if (service.attManager?.socket?.isConnected != true) {
service.attManager?.connect()
}
while (service.attManager?.socket?.isConnected != true) { while (service.attManager?.socket?.isConnected != true) {
delay(250) delay(250)
} }
@@ -545,21 +547,28 @@ class AirPodsViewModel(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val loudSoundReduction = val loudSoundReduction =
runCatching { service.attManager?.read(ATTHandles.LOUD_SOUND_REDUCTION) }.getOrNull() runCatching { service.attManager?.read(ATTHandles.LOUD_SOUND_REDUCTION) }.getOrNull()
val loudSoundReductionEnabled = loudSoundReduction?.size?.let {
if (it > 0) {
loudSoundReduction[0].toInt() == 1
} else false
}
val transparencyData = val transparencyData =
runCatching { service.attManager?.read(ATTHandles.TRANSPARENCY) }.getOrNull()?: byteArrayOf() runCatching { service.attManager?.read(ATTHandles.TRANSPARENCY) }.getOrNull()?: byteArrayOf()
val hearingAid = val hearingAidData =
runCatching { service.attManager?.read(ATTHandles.HEARING_AID) }.getOrNull()?: byteArrayOf() runCatching { service.attManager?.read(ATTHandles.HEARING_AID) }.getOrNull()?: byteArrayOf()
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
loudSoundReductionEnabled = loudSoundReduction?.get(0)?.toInt() == 0x01, loudSoundReductionEnabled = loudSoundReductionEnabled == true,
transparencyData = transparencyData, transparencyData = transparencyData,
hearingAidData = hearingAid hearingAidData = hearingAidData
) )
} }
} }
fun observeATT() { fun observeATT() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
service.attManager?.connect() if (service.attManager?.socket?.isConnected != true) {
service.attManager?.connect()
}
while (service.attManager?.socket?.isConnected != true) { while (service.attManager?.socket?.isConnected != true) {
delay(1000) delay(1000)
} }

View File

@@ -1019,7 +1019,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
) )
// Store in SharedPreferences // Store in SharedPreferences
sharedPreferences.edit { sharedPreferences.edit {
putString("airpods_name", deviceInformation.name) putString("name", deviceInformation.name)
putString("airpods_model_number", deviceInformation.modelNumber) putString("airpods_model_number", deviceInformation.modelNumber)
putString("airpods_manufacturer", deviceInformation.manufacturer) putString("airpods_manufacturer", deviceInformation.manufacturer)
putString("airpods_serial_number", deviceInformation.serialNumber) putString("airpods_serial_number", deviceInformation.serialNumber)
@@ -2388,16 +2388,27 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
?.getString("name", bluetoothDevice?.name) ?.getString("name", bluetoothDevice?.name)
if (bluetoothDevice != null && !action.isNullOrEmpty()) { if (bluetoothDevice != null && !action.isNullOrEmpty()) {
Log.d(TAG, "Received bluetooth connection broadcast: action=$action") Log.d(TAG, "Received bluetooth connection broadcast: action=$action")
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) { if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a") if (bluetoothDevice.uuids?.contains(uuid) == true) {
bluetoothDevice.fetchUuidsWithSdp() val intent = Intent(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
if (bluetoothDevice.uuids != null) { intent.putExtra("name", name)
if (bluetoothDevice.uuids.contains(uuid)) { intent.putExtra("device", bluetoothDevice)
val intent = Intent(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED) context?.sendBroadcast(intent)
intent.putExtra("name", name) } else {
intent.putExtra("device", bluetoothDevice) bluetoothDevice.fetchUuidsWithSdp()
context?.sendBroadcast(intent) }
} } else if ("android.bluetooth.device.action.UUID" == action) {
val savedMac = context?.getSharedPreferences("settings", MODE_PRIVATE)
?.getString("mac_address", "") ?: ""
val matchedByMac = savedMac.isNotEmpty() && bluetoothDevice.address == savedMac
val matchedByUuid = bluetoothDevice.uuids?.contains(uuid) == true
if (matchedByUuid || matchedByMac) {
val intent = Intent(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
intent.putExtra("name", name)
intent.putExtra("device", bluetoothDevice)
context?.sendBroadcast(intent)
} }
} }
} }
@@ -2421,6 +2432,32 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
takeOver("music", manualTakeOverAfterReversed = true) takeOver("music", manualTakeOverAfterReversed = true)
} }
val bluetoothManager = getSystemService(BluetoothManager::class.java)
val bluetoothAdapter = bluetoothManager.adapter
bluetoothAdapter?.bondedDevices?.forEach { device ->
device.fetchUuidsWithSdp()
if (device.uuids != null) {
// Check for the AirPods service UUID
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
if (device.uuids.contains(uuid)) {
Log.d(TAG, "Found AirPods device: ${device.name} (${device.address})")
// Connect or do whatever you need
CoroutineScope(Dispatchers.IO).launch {
connectToSocket(bluetoothAdapter, device)
}
setMetadatas(device)
macAddress = device.address
sharedPreferences.edit {
putString("mac_address", macAddress)
}
}
}
}
return START_STICKY return START_STICKY
} }
@@ -2682,8 +2719,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
BluetoothConnectionManager.setCurrentConnection(socket, device) BluetoothConnectionManager.setCurrentConnection(socket, device)
val xposedRemotePref = XposedRemotePrefProvider.create() val xposedRemotePref = XposedRemotePrefProvider.create()
if (xposedRemotePref.getBoolean("vendor_id_hook", false)) { if (xposedRemotePref.getBoolean("vendor_id_hook", false)) {
attManager = ATTManager(adapter, device) if (attManager == null) {
attManager!!.connect() attManager = ATTManager(adapter, device)
attManager!!.connect()
}
} }
// Create AirPodsInstance from stored config if available // Create AirPodsInstance from stored config if available