diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/AudioSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/AudioSettings.kt index 25a0c4e..059dc10 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/AudioSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/AudioSettings.kt @@ -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), diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/ConnectionSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/ConnectionSettings.kt index 58ffa14..28f796e 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/ConnectionSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/ConnectionSettings.kt @@ -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 diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/LoudSoundReductionSwitch.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/LoudSoundReductionSwitch.kt index 1d0ad9d..b79bd58 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/LoudSoundReductionSwitch.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/LoudSoundReductionSwitch.kt @@ -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) } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt index d920461..9878f1f 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt @@ -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) { 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 49ca9a8..76e31e1 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 @@ -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, diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt index f370957..09fceff 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt @@ -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)