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 androidx.compose.ui.unit.sp
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.services.ServiceManager import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.ATTManager
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@Composable @Composable
fun AudioSettings() { fun AudioSettings() {
val isDarkTheme = isSystemInDarkTheme() val isDarkTheme = isSystemInDarkTheme()
val textColor = if (isDarkTheme) Color.White else Color.Black 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(
text = stringResource(R.string.audio).uppercase(), text = stringResource(R.string.audio).uppercase(),
@@ -103,7 +84,7 @@ fun AudioSettings() {
.padding(start = 12.dp, end = 0.dp) .padding(start = 12.dp, end = 0.dp)
) )
LoudSoundReductionSwitch(attManager) LoudSoundReductionSwitch()
HorizontalDivider( HorizontalDivider(
thickness = 1.5.dp, thickness = 1.5.dp,
color = Color(0x40888888), color = Color(0x40888888),

View File

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

View File

@@ -50,26 +50,26 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.ATTManager import me.kavishdevar.librepods.utils.ATTManager
import me.kavishdevar.librepods.utils.ATTHandles
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@Composable @Composable
fun LoudSoundReductionSwitch(attManager: ATTManager) { fun LoudSoundReductionSwitch() {
var loudSoundReductionEnabled by remember { var loudSoundReductionEnabled by remember {
mutableStateOf( mutableStateOf(
false false
) )
} }
val attManager = ServiceManager.getService()?.attManager ?: throw IllegalStateException("ATTManager not available")
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
while (attManager.socket?.isConnected != true) { attManager.enableNotifications(ATTHandles.LOUD_SOUND_REDUCTION)
delay(100)
}
attManager.enableNotifications(0x1b)
var parsed = false var parsed = false
for (attempt in 1..3) { for (attempt in 1..3) {
try { try {
val data = attManager.read(0x1b) val data = attManager.read(ATTHandles.LOUD_SOUND_REDUCTION)
if (data.size == 2) { if (data.size == 2) {
loudSoundReductionEnabled = data[1].toInt() != 0 loudSoundReductionEnabled = data[1].toInt() != 0
Log.d("LoudSoundReduction", "Read attempt $attempt: enabled=${loudSoundReductionEnabled}") Log.d("LoudSoundReduction", "Read attempt $attempt: enabled=${loudSoundReductionEnabled}")
@@ -90,7 +90,7 @@ fun LoudSoundReductionSwitch(attManager: ATTManager) {
LaunchedEffect(loudSoundReductionEnabled) { LaunchedEffect(loudSoundReductionEnabled) {
if (attManager.socket?.isConnected != true) return@LaunchedEffect 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 { val loudSoundListener = remember {
@@ -107,12 +107,12 @@ fun LoudSoundReductionSwitch(attManager: ATTManager) {
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
attManager.registerListener(0x1b, loudSoundListener) attManager.registerListener(ATTHandles.LOUD_SOUND_REDUCTION, loudSoundListener)
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { 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.HazeEffectScope
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.CupertinoMaterials import dev.chrisbanes.haze.materials.CupertinoMaterials
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -102,6 +103,7 @@ import me.kavishdevar.librepods.composables.ToneVolumeSlider
import me.kavishdevar.librepods.composables.VolumeControlSwitch import me.kavishdevar.librepods.composables.VolumeControlSwitch
import me.kavishdevar.librepods.services.ServiceManager import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.ATTManager import me.kavishdevar.librepods.utils.ATTManager
import me.kavishdevar.librepods.utils.ATTHandles
import me.kavishdevar.librepods.utils.AACPManager import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.RadareOffsetFinder import me.kavishdevar.librepods.utils.RadareOffsetFinder
import java.io.IOException import java.io.IOException
@@ -123,7 +125,7 @@ fun AccessibilitySettingsScreen() {
val verticalScrollState = rememberScrollState() val verticalScrollState = rememberScrollState()
val hazeState = remember { HazeState() } val hazeState = remember { HazeState() }
val snackbarHostState = remember { SnackbarHostState() } 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) // get the AACP manager if available (used for EQ read/write)
val aacpManager = remember { ServiceManager.getService()?.aacpManager } val aacpManager = remember { ServiceManager.getService()?.aacpManager }
val context = LocalContext.current val context = LocalContext.current
@@ -135,17 +137,6 @@ fun AccessibilitySettingsScreen() {
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF) val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
val labelTextColor = if (isDarkTheme) Color.White else Color.Black 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( Scaffold(
containerColor = if (isSystemInDarkTheme()) Color( containerColor = if (isSystemInDarkTheme()) Color(
0xFF000000 0xFF000000
@@ -198,6 +189,7 @@ fun AccessibilitySettingsScreen() {
) { paddingValues -> ) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
.hazeSource(hazeState)
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
@@ -367,20 +359,15 @@ fun AccessibilitySettingsScreen() {
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
attManager.unregisterListener(0x18, transparencyListener) attManager.unregisterListener(ATTHandles.TRANSPARENCY, transparencyListener)
} }
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
Log.d(TAG, "Connecting to ATT...") Log.d(TAG, "Connecting to ATT...")
try { try {
attManager.connect() attManager.enableNotifications(ATTHandles.TRANSPARENCY)
while (attManager.socket?.isConnected != true) { attManager.registerListener(ATTHandles.TRANSPARENCY, transparencyListener)
delay(100)
}
attManager.enableNotifications(0x18)
attManager.registerListener(0x18, transparencyListener)
// If we have an AACP manager, prefer its EQ data to populate EQ controls first // If we have an AACP manager, prefer its EQ data to populate EQ controls first
try { try {
@@ -407,7 +394,7 @@ fun AccessibilitySettingsScreen() {
for (attempt in 1..3) { for (attempt in 1..3) {
initialReadAttempts.value = attempt initialReadAttempts.value = attempt
try { try {
val data = attManager.read(0x18) val data = attManager.read(ATTHandles.TRANSPARENCY)
parsedSettings = parseTransparencySettingsResponse(data = data) parsedSettings = parseTransparencySettingsResponse(data = data)
if (parsedSettings != null) { if (parsedSettings != null) {
Log.d(TAG, "Parsed settings on attempt $attempt") Log.d(TAG, "Parsed settings on attempt $attempt")
@@ -569,7 +556,7 @@ fun AccessibilitySettingsScreen() {
ToneVolumeSlider() ToneVolumeSlider()
SinglePodANCSwitch() SinglePodANCSwitch()
VolumeControlSwitch() VolumeControlSwitch()
LoudSoundReductionSwitch(attManager) LoudSoundReductionSwitch()
DropdownMenuComponent( DropdownMenuComponent(
label = "Press Speed", label = "Press Speed",
@@ -1113,7 +1100,7 @@ private fun sendTransparencySettings(
val data = buffer.array() val data = buffer.array()
attManager.write( attManager.write(
0x18, ATTHandles.TRANSPARENCY,
value = data value = data
) )
} catch (e: IOException) { } 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.constants.isHeadTrackingData
import me.kavishdevar.librepods.utils.AACPManager import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType 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.BLEManager
import me.kavishdevar.librepods.utils.BluetoothConnectionManager import me.kavishdevar.librepods.utils.BluetoothConnectionManager
import me.kavishdevar.librepods.utils.CrossDevice import me.kavishdevar.librepods.utils.CrossDevice
@@ -148,6 +149,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
var macAddress = "" var macAddress = ""
var localMac = "" var localMac = ""
lateinit var aacpManager: AACPManager lateinit var aacpManager: AACPManager
var attManager: ATTManager? = null
var cameraActive = false var cameraActive = false
private var disconnectedBecauseReversed = false private var disconnectedBecauseReversed = false
data class ServiceConfig( data class ServiceConfig(
@@ -634,6 +636,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
isConnectedLocally = false isConnectedLocally = false
popupShown = false popupShown = false
updateNotificationContent(false) updateNotificationContent(false)
attManager?.disconnect()
attManager = null
} }
} }
} }
@@ -2294,6 +2298,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
BluetoothConnectionManager.setCurrentConnection(socket, device) BluetoothConnectionManager.setCurrentConnection(socket, device)
attManager = ATTManager(device)
attManager!!.connect()
updateNotificationContent( updateNotificationContent(
true, true,
config.deviceName, config.deviceName,

View File

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