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 aed865cf..e8273e6a 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 @@ -1143,7 +1143,7 @@ class AACPManager { ) } - val socket = BluetoothConnectionManager.getCurrentSocket() ?: return false + val socket = BluetoothConnectionManager.getAACPSocket() ?: return false if (socket.isConnected) { socket.outputStream?.write(packet) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/ATTManagerv2.kt b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/ATTManagerv2.kt new file mode 100644 index 00000000..2348336c --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/ATTManagerv2.kt @@ -0,0 +1,63 @@ +package me.kavishdevar.librepods.bluetooth + +import android.util.Log + +private const val TAG = "ATTManagerv2" + +// the random disconnects were because of ATT, apparently. seems like we will have to accept no notifications for external changes (mainly amplification in hearing aid) +object ATTManagerv2 { + fun readCharacteristic(handle: ATTHandles): ByteArray? { + val socket = BluetoothConnectionManager.getATTSocket()?: return null + try { +// socket.connect() + val input = socket.inputStream + val output = socket.outputStream + + val pdu = byteArrayOf(0x0A, handle.value.toByte(), 0x00) + output.write(pdu) + output.flush() + Log.d(TAG, "writeRaw: ${pdu.joinToString(" ") { String.format("%02X", it) }}") + val buffer = ByteArray(512) + val len = input.read(buffer) + if (len == -1) { + throw IllegalStateException("End of stream reached") + } + val data = buffer.copyOfRange(0, len) +// socket.close() + if (data[0] != 0x0B.toByte()) { + throw IllegalStateException("Invalid response: ${data.joinToString(" ") { String.format("%02X", it) }}") + } + Log.d(TAG, "readPDU: ${data.joinToString(" ") { String.format("%02X", it) }}") + return data.copyOfRange(1, data.size) + } catch (e: Exception) { + Log.e(TAG, "Error reading characteristic: ${e.message}") + return null + } + } + + fun writeCharacteristic(handle: ATTHandles, data: ByteArray) { + val socket = BluetoothConnectionManager.getATTSocket()?: return + try { +// socket.connect() + val input = socket.inputStream + val output = socket.outputStream + val pdu = byteArrayOf(0x12, handle.value.toByte(), 0x00) + data // 0x0 because LE + output.write(pdu) + output.flush() + Log.d(TAG, "writeRaw: ${pdu.joinToString(" ") { String.format("%02X", it) }}") + val buffer = ByteArray(512) + val len = input.read(buffer) + if (len == -1) { + throw IllegalStateException("End of stream reached") + } + val resp = buffer.copyOfRange(0, len) +// socket.close() + if (!resp.contentEquals(byteArrayOf(0x13))) { + throw IllegalStateException("Invalid response: ${resp.joinToString(" ") { String.format("%02X", it) }}") + } + Log.d(TAG, "readPDU: ${resp.joinToString(" ") { String.format("%02X", it) }}") + } catch (e: Exception) { + Log.e(TAG, "Error writing characteristic: ${e.message}") + } + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/BluetoothConnectionManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/BluetoothConnectionManager.kt index d98050be..012b587d 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/BluetoothConnectionManager.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/BluetoothConnectionManager.kt @@ -18,23 +18,22 @@ package me.kavishdevar.librepods.bluetooth -import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothSocket -import android.util.Log object BluetoothConnectionManager { - private const val TAG = "BluetoothConnectionManager" + private var aacpSocket: BluetoothSocket? = null + private var attSocket: BluetoothSocket? = null - private var currentSocket: BluetoothSocket? = null - private var currentDevice: BluetoothDevice? = null - - fun setCurrentConnection(socket: BluetoothSocket, device: BluetoothDevice) { - currentSocket = socket - currentDevice = device - Log.d(TAG, "Current connection set to device: ${device.address}") + fun setCurrentConnection(aacpSocket: BluetoothSocket?, attSocket: BluetoothSocket?) { + BluetoothConnectionManager.aacpSocket = aacpSocket + BluetoothConnectionManager.attSocket = attSocket } - fun getCurrentSocket(): BluetoothSocket? { - return currentSocket + fun getAACPSocket(): BluetoothSocket? { + return aacpSocket + } + + fun getATTSocket(): BluetoothSocket? { + return attSocket } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/data/HearingAid.kt b/android/app/src/main/java/me/kavishdevar/librepods/data/HearingAid.kt index bf2f554c..d0b64441 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/data/HearingAid.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/data/HearingAid.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.kavishdevar.librepods.bluetooth.ATTHandles import me.kavishdevar.librepods.bluetooth.ATTManager +import me.kavishdevar.librepods.bluetooth.ATTManagerv2 import java.io.IOException import java.nio.ByteBuffer import java.nio.ByteOrder @@ -138,7 +139,7 @@ fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings? { } fun sendHearingAidSettings( - attManager: ATTManager, +// attManager: ATTManager, hearingAidSettings: HearingAidSettings, debounceJob: MutableState ) { @@ -146,7 +147,7 @@ fun sendHearingAidSettings( debounceJob.value = CoroutineScope(Dispatchers.IO).launch { delay(100) try { - val currentData = attManager.read(ATTHandles.HEARING_AID) + val currentData = ATTManagerv2.readCharacteristic(ATTHandles.HEARING_AID)?: return@launch Log.d(TAG, "Current data before update: ${currentData.joinToString(" ") { String.format("%02X", it) }}") if (currentData.size < 104) { Log.w(TAG, "Current data size ${currentData.size} too small, cannot send settings") @@ -184,7 +185,7 @@ fun sendHearingAidSettings( Log.d(TAG, "Sending updated settings: ${currentData.joinToString(" ") { String.format("%02X", it) }}") - attManager.write(ATTHandles.HEARING_AID, currentData) + ATTManagerv2.writeCharacteristic(ATTHandles.HEARING_AID, currentData) } catch (e: IOException) { e.printStackTrace() } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidAdjustmentsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidAdjustmentsScreen.kt index 92dff14f..7a41019a 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidAdjustmentsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidAdjustmentsScreen.kt @@ -56,12 +56,14 @@ import me.kavishdevar.librepods.presentation.components.StyledToggle import me.kavishdevar.librepods.services.ServiceManager import me.kavishdevar.librepods.bluetooth.AACPManager import me.kavishdevar.librepods.bluetooth.ATTHandles +import me.kavishdevar.librepods.bluetooth.ATTManagerv2 import me.kavishdevar.librepods.data.HearingAidSettings import me.kavishdevar.librepods.data.parseHearingAidSettingsResponse import me.kavishdevar.librepods.data.sendHearingAidSettings import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel import java.io.IOException import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.time.Duration.Companion.milliseconds private var debounceJob: MutableState = mutableStateOf(null) private const val TAG = "HearingAidAdjustments" @@ -74,7 +76,7 @@ fun HearingAidAdjustmentsScreen(viewModel: AirPodsViewModel) { isSystemInDarkTheme() val verticalScrollState = rememberScrollState() val hazeState = remember { HazeState() } - val attManager = ServiceManager.getService()?.attManager ?: throw IllegalStateException("ATTManager not available") +// val attManager = ServiceManager.getService()?.attManager ?: throw IllegalStateException("ATTManager not available") val state by viewModel.uiState.collectAsState() @@ -175,20 +177,20 @@ fun HearingAidAdjustmentsScreen(viewModel: AirPodsViewModel) { ownVoiceAmplification = ownVoiceAmplification.floatValue ) Log.d(TAG, "Updated settings: ${hearingAidSettings.value}") - sendHearingAidSettings(attManager, hearingAidSettings.value, debounceJob) + sendHearingAidSettings(hearingAidSettings.value, debounceJob) } LaunchedEffect(Unit) { Log.d(TAG, "Connecting to ATT...") try { - attManager.enableNotifications(ATTHandles.HEARING_AID) - attManager.registerListener(ATTHandles.HEARING_AID, hearingAidATTListener) +// attManager.enableNotifications(ATTHandles.HEARING_AID) +// attManager.registerListener(ATTHandles.HEARING_AID, hearingAidATTListener) var parsedSettings: HearingAidSettings? = null for (attempt in 1..3) { initialReadAttempts.intValue = attempt try { - val data = attManager.read(ATTHandles.HEARING_AID) + val data = ATTManagerv2.readCharacteristic(ATTHandles.HEARING_AID) ?: return@LaunchedEffect parsedSettings = parseHearingAidSettingsResponse(data = data) if (parsedSettings != null) { Log.d(TAG, "Parsed settings on attempt $attempt") @@ -199,7 +201,7 @@ fun HearingAidAdjustmentsScreen(viewModel: AirPodsViewModel) { } catch (e: Exception) { Log.w(TAG, "Read attempt $attempt failed: ${e.message}") } - delay(200) + delay(200.milliseconds) } if (parsedSettings != null) { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/UpdateHearingTestScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/UpdateHearingTestScreen.kt index 635c75ba..4a4c534f 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/UpdateHearingTestScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/UpdateHearingTestScreen.kt @@ -62,6 +62,7 @@ import me.kavishdevar.librepods.R import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.services.ServiceManager import me.kavishdevar.librepods.bluetooth.ATTHandles +import me.kavishdevar.librepods.bluetooth.ATTManagerv2 import me.kavishdevar.librepods.data.HearingAidSettings import me.kavishdevar.librepods.data.parseHearingAidSettingsResponse import me.kavishdevar.librepods.data.sendHearingAidSettings @@ -73,17 +74,17 @@ private const val TAG = "HearingAidAdjustments" @Composable fun UpdateHearingTestScreen() { val verticalScrollState = rememberScrollState() - val attManager = ServiceManager.getService()?.attManager - if (attManager == null) { - Text( - text = stringResource(R.string.att_manager_is_null_try_reconnecting), - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - textAlign = TextAlign.Center - ) - return - } +// val attManager = ServiceManager.getService()?.attManager +// if (attManager == null) { +// Text( +// text = stringResource(R.string.att_manager_is_null_try_reconnecting), +// modifier = Modifier +// .fillMaxSize() +// .padding(16.dp), +// textAlign = TextAlign.Center +// ) +// return +// } val backdrop = rememberLayerBackdrop() StyledScaffold( @@ -167,11 +168,11 @@ fun UpdateHearingTestScreen() { } - DisposableEffect(Unit) { - onDispose { - attManager.unregisterListener(ATTHandles.HEARING_AID, hearingAidATTListener) - } - } +// DisposableEffect(Unit) { +// onDispose { +// attManager.unregisterListener(ATTHandles.HEARING_AID, hearingAidATTListener) +// } +// } LaunchedEffect( leftEQ.value, @@ -214,20 +215,20 @@ fun UpdateHearingTestScreen() { ownVoiceAmplification = ownVoiceAmplification.floatValue ) Log.d(TAG, "Updated settings: ${hearingAidSettings.value}") - sendHearingAidSettings(attManager, hearingAidSettings.value, debounceJob) + sendHearingAidSettings(hearingAidSettings.value, debounceJob) } LaunchedEffect(Unit) { Log.d(TAG, "Connecting to ATT...") try { - attManager.enableNotifications(ATTHandles.HEARING_AID) - attManager.registerListener(ATTHandles.HEARING_AID, hearingAidATTListener) +// attManager.enableNotifications(ATTHandles.HEARING_AID) +// attManager.registerListener(ATTHandles.HEARING_AID, hearingAidATTListener) var parsedSettings: HearingAidSettings? = null for (attempt in 1..3) { initialReadAttempts.intValue = attempt try { - val data = attManager.read(ATTHandles.HEARING_AID) + val data = ATTManagerv2.readCharacteristic(ATTHandles.HEARING_AID)?: byteArrayOf() parsedSettings = parseHearingAidSettingsResponse(data = data) if (parsedSettings != null) { Log.d(TAG, "Parsed settings on attempt $attempt") diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt index e9ed96cf..9570cb21 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt @@ -40,6 +40,7 @@ import me.kavishdevar.librepods.billing.BillingManager import me.kavishdevar.librepods.bluetooth.AACPManager import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.ControlCommandIdentifiers import me.kavishdevar.librepods.bluetooth.ATTHandles +import me.kavishdevar.librepods.bluetooth.ATTManagerv2 import me.kavishdevar.librepods.data.AirPodsInstance import me.kavishdevar.librepods.data.AirPodsModels import me.kavishdevar.librepods.data.AirPodsNotifications @@ -51,6 +52,7 @@ import me.kavishdevar.librepods.data.ControlCommandRepository import me.kavishdevar.librepods.data.StemAction import me.kavishdevar.librepods.data.XposedRemotePrefProvider import me.kavishdevar.librepods.services.AirPodsService +import kotlin.time.Duration.Companion.milliseconds @Suppress("ArrayInDataClass") data class AirPodsUiState( @@ -530,13 +532,7 @@ class AirPodsViewModel( } viewModelScope.launch(Dispatchers.IO) { try { - if (service.attManager?.socket?.isConnected != true) { - service.attManager?.connect() - } - while (service.attManager?.socket?.isConnected != true) { - delay(250) - } - service.attManager?.write(handle, value) + ATTManagerv2.writeCharacteristic(handle, value) } catch (e: Exception) { e.printStackTrace() } @@ -545,19 +541,14 @@ class AirPodsViewModel( fun refreshATT() { viewModelScope.launch(Dispatchers.IO) { - val loudSoundReduction = - runCatching { service.attManager?.read(ATTHandles.LOUD_SOUND_REDUCTION) }.getOrNull() - val loudSoundReductionEnabled = loudSoundReduction?.size?.let { - if (it > 0) { + val loudSoundReduction = ATTManagerv2.readCharacteristic(ATTHandles.LOUD_SOUND_REDUCTION) ?: byteArrayOf() + val loudSoundReductionEnabled = if (loudSoundReduction.isNotEmpty()) { loudSoundReduction[0].toInt() == 1 - } else false - } - val transparencyData = - runCatching { service.attManager?.read(ATTHandles.TRANSPARENCY) }.getOrNull()?: byteArrayOf() - val hearingAidData = - runCatching { service.attManager?.read(ATTHandles.HEARING_AID) }.getOrNull()?: byteArrayOf() + } else false + val transparencyData = ATTManagerv2.readCharacteristic(ATTHandles.TRANSPARENCY)?: byteArrayOf() + val hearingAidData = ATTManagerv2.readCharacteristic(ATTHandles.HEARING_AID)?:byteArrayOf() _uiState.value = _uiState.value.copy( - loudSoundReductionEnabled = loudSoundReductionEnabled == true, + loudSoundReductionEnabled = loudSoundReductionEnabled, transparencyData = transparencyData, hearingAidData = hearingAidData ) @@ -566,19 +557,9 @@ class AirPodsViewModel( fun observeATT() { viewModelScope.launch(Dispatchers.IO) { - if (service.attManager?.socket?.isConnected != true) { - service.attManager?.connect() - } - while (service.attManager?.socket?.isConnected != true) { - delay(1000) - } - service.attManager?.enableNotifications(ATTHandles.LOUD_SOUND_REDUCTION) - service.attManager?.enableNotifications(ATTHandles.TRANSPARENCY) - service.attManager?.enableNotifications(ATTHandles.HEARING_AID) - while (true) { refreshATT() - delay(15000) + delay(15000.milliseconds) } } } 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 0f15d62a..4f5eee67 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 @@ -129,6 +129,7 @@ import java.nio.ByteOrder import java.time.LocalDateTime import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.time.Duration.Companion.milliseconds private const val TAG = "AirPodsService" @@ -151,7 +152,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList var macAddress = "" var localMac = "" lateinit var aacpManager: AACPManager - var attManager: ATTManager? = null var airpodsInstance: AirPodsInstance? = null var cameraActive = false private var disconnectedBecauseReversed = false @@ -654,6 +654,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList addAction("android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT") addAction("android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED") addAction("android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED") + addAction("android.bluetooth.device.action.UUID") } connectionReceiver = object : BroadcastReceiver() { @@ -691,8 +692,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList // isConnectedLocally = false popupShown = false updateNotificationContent(false) - attManager?.disconnect() - attManager = null + aacpManager.disconnected() + BluetoothConnectionManager.getATTSocket()?.close() + BluetoothConnectionManager.setCurrentConnection(null, null) } } } @@ -2432,32 +2434,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList 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 } @@ -2647,15 +2623,15 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } private fun createBluetoothSocket( - adapter: BluetoothAdapter, device: BluetoothDevice, uuid: ParcelUuid + adapter: BluetoothAdapter, device: BluetoothDevice, uuid: ParcelUuid, psm: Int ): BluetoothSocket { val type = 3 // L2CAP val constructorSpecs = listOf( - arrayOf(adapter, device, type, true, true, 0x1001, uuid), // A16QPR3 - arrayOf(device, type, true, true, 0x1001, uuid), - arrayOf(device, type, 1, true, true, 0x1001, uuid), - arrayOf(type, 1, true, true, device, 0x1001, uuid), - arrayOf(type, true, true, device, 0x1001, uuid) + arrayOf(adapter, device, type, true, true, psm, uuid), // A16QPR3 + arrayOf(device, type, true, true, psm, uuid), + arrayOf(device, type, 1, true, true, psm, uuid), + arrayOf(type, 1, true, true, device, psm, uuid), + arrayOf(type, true, true, device, psm, uuid) ) val constructors = BluetoothSocket::class.java.declaredConstructors @@ -2701,7 +2677,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a") // if (!isConnectedLocally) { socket = try { - createBluetoothSocket(adapter, device, uuid) + 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}") @@ -2710,20 +2686,22 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList try { runBlocking { - withTimeout(5000L) { + withTimeout(5000.milliseconds) { try { socket.connect() // isConnectedLocally = true this@AirPodsService.device = device - - BluetoothConnectionManager.setCurrentConnection(socket, device) val xposedRemotePref = XposedRemotePrefProvider.create() - if (xposedRemotePref.getBoolean("vendor_id_hook", false)) { - if (attManager == null) { - attManager = ATTManager(adapter, device) - attManager!!.connect() - } - } + val attSocket = if (xposedRemotePref.getBoolean("vendor_id_hook", false)) { + createBluetoothSocket( + adapter, + device, + ParcelUuid.fromString("00000000-0000-0000-0000-000000000000"), + 31 + ) + } else null + attSocket?.connect() + BluetoothConnectionManager.setCurrentConnection(socket, attSocket) // Create AirPodsInstance from stored config if available if (airpodsInstance == null && config.airpodsModelNumber.isNotEmpty()) { @@ -2928,7 +2906,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList socket.close() // isConnectedLocally = false aacpManager.disconnected() - attManager?.disconnect() + BluetoothConnectionManager.getATTSocket()?.close() + BluetoothConnectionManager.setCurrentConnection(null, null) updateNotificationContent(false) sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply { setPackage(packageName)