android: move attmanager to service to avoid trying to connect multiple times

This commit is contained in:
Kavish Devar
2025-09-21 21:44:54 +05:30
parent ecfdc05dbf
commit 3ace0e1831
6 changed files with 64 additions and 65 deletions

View File

@@ -42,31 +42,12 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.ATTManager
import kotlin.io.encoding.ExperimentalEncodingApi
@Composable
fun AudioSettings() {
val isDarkTheme = isSystemInDarkTheme()
val textColor = if (isDarkTheme) Color.White else Color.Black
val attManager = ATTManager(ServiceManager.getService()?.device?: throw IllegalStateException("No device connected"))
DisposableEffect(attManager) {
onDispose {
try {
attManager.disconnect()
} catch (e: Exception) {
Log.w("AirPodsAudioSettings", "Error while disconnecting ATTManager: ${e.message}")
}
}
}
LaunchedEffect(Unit) {
Log.d("AirPodsAudioSettings", "Connecting to ATT...")
try {
attManager.connect()
} catch (e: Exception) {
Log.w("AirPodsAudioSettings", "Error while connecting ATTManager: ${e.message}")
}
}
Text(
text = stringResource(R.string.audio).uppercase(),
@@ -103,7 +84,7 @@ fun AudioSettings() {
.padding(start = 12.dp, end = 0.dp)
)
LoudSoundReductionSwitch(attManager)
LoudSoundReductionSwitch()
HorizontalDivider(
thickness = 1.5.dp,
color = Color(0x40888888),

View File

@@ -42,7 +42,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.ATTManager
import kotlin.io.encoding.ExperimentalEncodingApi
@Composable

View File

@@ -50,26 +50,26 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.ATTManager
import me.kavishdevar.librepods.utils.ATTHandles
import kotlin.io.encoding.ExperimentalEncodingApi
@Composable
fun LoudSoundReductionSwitch(attManager: ATTManager) {
fun LoudSoundReductionSwitch() {
var loudSoundReductionEnabled by remember {
mutableStateOf(
false
)
}
val attManager = ServiceManager.getService()?.attManager ?: throw IllegalStateException("ATTManager not available")
LaunchedEffect(Unit) {
while (attManager.socket?.isConnected != true) {
delay(100)
}
attManager.enableNotifications(0x1b)
attManager.enableNotifications(ATTHandles.LOUD_SOUND_REDUCTION)
var parsed = false
for (attempt in 1..3) {
try {
val data = attManager.read(0x1b)
val data = attManager.read(ATTHandles.LOUD_SOUND_REDUCTION)
if (data.size == 2) {
loudSoundReductionEnabled = data[1].toInt() != 0
Log.d("LoudSoundReduction", "Read attempt $attempt: enabled=${loudSoundReductionEnabled}")
@@ -90,7 +90,7 @@ fun LoudSoundReductionSwitch(attManager: ATTManager) {
LaunchedEffect(loudSoundReductionEnabled) {
if (attManager.socket?.isConnected != true) return@LaunchedEffect
attManager.write(0x1b, if (loudSoundReductionEnabled) byteArrayOf(1) else byteArrayOf(0))
attManager.write(ATTHandles.LOUD_SOUND_REDUCTION, if (loudSoundReductionEnabled) byteArrayOf(1) else byteArrayOf(0))
}
val loudSoundListener = remember {
@@ -107,12 +107,12 @@ fun LoudSoundReductionSwitch(attManager: ATTManager) {
}
LaunchedEffect(Unit) {
attManager.registerListener(0x1b, loudSoundListener)
attManager.registerListener(ATTHandles.LOUD_SOUND_REDUCTION, loudSoundListener)
}
DisposableEffect(Unit) {
onDispose {
attManager.unregisterListener(0x1b, loudSoundListener)
attManager.unregisterListener(ATTHandles.LOUD_SOUND_REDUCTION, loudSoundListener)
}
}

View File

@@ -86,6 +86,7 @@ import androidx.compose.ui.unit.sp
import dev.chrisbanes.haze.HazeEffectScope
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.CupertinoMaterials
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.CoroutineScope
@@ -102,6 +103,7 @@ import me.kavishdevar.librepods.composables.ToneVolumeSlider
import me.kavishdevar.librepods.composables.VolumeControlSwitch
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.ATTManager
import me.kavishdevar.librepods.utils.ATTHandles
import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.RadareOffsetFinder
import java.io.IOException
@@ -123,7 +125,7 @@ fun AccessibilitySettingsScreen() {
val verticalScrollState = rememberScrollState()
val hazeState = remember { HazeState() }
val snackbarHostState = remember { SnackbarHostState() }
val attManager = ATTManager(ServiceManager.getService()?.device?: throw IllegalStateException("No device connected"))
val attManager = ServiceManager.getService()?.attManager ?: throw IllegalStateException("ATTManager not available")
// get the AACP manager if available (used for EQ read/write)
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
val context = LocalContext.current
@@ -135,17 +137,6 @@ fun AccessibilitySettingsScreen() {
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
DisposableEffect(attManager) {
onDispose {
Log.d(TAG, "Disconnecting from ATT...")
try {
attManager.disconnect()
} catch (e: Exception) {
Log.w(TAG, "Error while disconnecting ATTManager: ${e.message}")
}
}
}
Scaffold(
containerColor = if (isSystemInDarkTheme()) Color(
0xFF000000
@@ -198,6 +189,7 @@ fun AccessibilitySettingsScreen() {
) { paddingValues ->
Column(
modifier = Modifier
.hazeSource(hazeState)
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 16.dp)
@@ -367,20 +359,15 @@ fun AccessibilitySettingsScreen() {
DisposableEffect(Unit) {
onDispose {
attManager.unregisterListener(0x18, transparencyListener)
attManager.unregisterListener(ATTHandles.TRANSPARENCY, transparencyListener)
}
}
LaunchedEffect(Unit) {
Log.d(TAG, "Connecting to ATT...")
try {
attManager.connect()
while (attManager.socket?.isConnected != true) {
delay(100)
}
attManager.enableNotifications(0x18)
attManager.registerListener(0x18, transparencyListener)
attManager.enableNotifications(ATTHandles.TRANSPARENCY)
attManager.registerListener(ATTHandles.TRANSPARENCY, transparencyListener)
// If we have an AACP manager, prefer its EQ data to populate EQ controls first
try {
@@ -407,7 +394,7 @@ fun AccessibilitySettingsScreen() {
for (attempt in 1..3) {
initialReadAttempts.value = attempt
try {
val data = attManager.read(0x18)
val data = attManager.read(ATTHandles.TRANSPARENCY)
parsedSettings = parseTransparencySettingsResponse(data = data)
if (parsedSettings != null) {
Log.d(TAG, "Parsed settings on attempt $attempt")
@@ -569,7 +556,7 @@ fun AccessibilitySettingsScreen() {
ToneVolumeSlider()
SinglePodANCSwitch()
VolumeControlSwitch()
LoudSoundReductionSwitch(attManager)
LoudSoundReductionSwitch()
DropdownMenuComponent(
label = "Press Speed",
@@ -1113,7 +1100,7 @@ private fun sendTransparencySettings(
val data = buffer.array()
attManager.write(
0x18,
ATTHandles.TRANSPARENCY,
value = data
)
} catch (e: IOException) {

View File

@@ -88,6 +88,7 @@ import me.kavishdevar.librepods.constants.StemAction
import me.kavishdevar.librepods.constants.isHeadTrackingData
import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType
import me.kavishdevar.librepods.utils.ATTManager
import me.kavishdevar.librepods.utils.BLEManager
import me.kavishdevar.librepods.utils.BluetoothConnectionManager
import me.kavishdevar.librepods.utils.CrossDevice
@@ -148,6 +149,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
var macAddress = ""
var localMac = ""
lateinit var aacpManager: AACPManager
var attManager: ATTManager? = null
var cameraActive = false
private var disconnectedBecauseReversed = false
data class ServiceConfig(
@@ -634,6 +636,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
isConnectedLocally = false
popupShown = false
updateNotificationContent(false)
attManager?.disconnect()
attManager = null
}
}
}
@@ -2294,6 +2298,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
BluetoothConnectionManager.setCurrentConnection(socket, device)
attManager = ATTManager(device)
attManager!!.connect()
updateNotificationContent(
true,
config.deviceName,

View File

@@ -37,6 +37,18 @@ import java.io.OutputStream
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
enum class ATTHandles(val value: Int) {
TRANSPARENCY(0x18),
LOUD_SOUND_REDUCTION(0x1b),
HEARING_AID(0x2a),
}
enum class ATTCCCDHandles(val value: Int) {
TRANSPARENCY(ATTHandles.TRANSPARENCY.value + 1),
LOUD_SOUND_REDUCTION(ATTHandles.LOUD_SOUND_REDUCTION.value + 1),
HEARING_AID(ATTHandles.HEARING_AID.value + 1),
}
class ATTManager(private val device: BluetoothDevice) {
companion object {
private const val TAG = "ATTManager"
@@ -103,30 +115,43 @@ class ATTManager(private val device: BluetoothDevice) {
}
}
fun registerListener(handle: Int, listener: (ByteArray) -> Unit) {
listeners.getOrPut(handle) { mutableListOf() }.add(listener)
fun registerListener(handle: ATTHandles, listener: (ByteArray) -> Unit) {
listeners.getOrPut(handle.value) { mutableListOf() }.add(listener)
}
fun unregisterListener(handle: Int, listener: (ByteArray) -> Unit) {
listeners[handle]?.remove(listener)
fun unregisterListener(handle: ATTHandles, listener: (ByteArray) -> Unit) {
listeners[handle.value]?.remove(listener)
}
fun enableNotifications(handle: Int) {
write(handle + 1, byteArrayOf(0x01, 0x00))
fun enableNotifications(handle: ATTHandles) {
write(ATTCCCDHandles.valueOf(handle.name), byteArrayOf(0x01, 0x00))
}
fun read(handle: Int): ByteArray {
val lsb = (handle and 0xFF).toByte()
val msb = ((handle shr 8) and 0xFF).toByte()
fun read(handle: ATTHandles): ByteArray {
val lsb = (handle.value and 0xFF).toByte()
val msb = ((handle.value shr 8) and 0xFF).toByte()
val pdu = byteArrayOf(OPCODE_READ_REQUEST, lsb, msb)
writeRaw(pdu)
// wait for response placed into responses queue by the reader coroutine
return readResponse()
}
fun write(handle: Int, value: ByteArray) {
val lsb = (handle and 0xFF).toByte()
val msb = ((handle shr 8) and 0xFF).toByte()
fun write(handle: ATTHandles, value: ByteArray) {
val lsb = (handle.value and 0xFF).toByte()
val msb = ((handle.value shr 8) and 0xFF).toByte()
val pdu = byteArrayOf(OPCODE_WRITE_REQUEST, lsb, msb) + value
writeRaw(pdu)
// usually a Write Response (0x13) will arrive; wait for it (but discard return)
try {
readResponse()
} catch (e: Exception) {
Log.w(TAG, "No write response received: ${e.message}")
}
}
fun write(handle: ATTCCCDHandles, value: ByteArray) {
val lsb = (handle.value and 0xFF).toByte()
val msb = ((handle.value shr 8) and 0xFF).toByte()
val pdu = byteArrayOf(OPCODE_WRITE_REQUEST, lsb, msb) + value
writeRaw(pdu)
// usually a Write Response (0x13) will arrive; wait for it (but discard return)