android: add hearing aid adjustments

This commit is contained in:
Kavish Devar
2025-09-22 14:54:54 +05:30
parent ce229bec6e
commit 4751f70579
7 changed files with 505 additions and 324 deletions

View File

@@ -0,0 +1,180 @@
package me.kavishdevar.librepods.composables
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.materials.CupertinoMaterials
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import me.kavishdevar.librepods.R
@ExperimentalHazeMaterialsApi
@Composable
fun ConfirmationDialog(
showDialog: MutableState<Boolean>,
title: String,
message: String,
confirmText: String = "Enable",
dismissText: String = "Cancel",
onConfirm: () -> Unit,
onDismiss: () -> Unit = { showDialog.value = false },
hazeState: HazeState,
isDarkTheme: Boolean,
textColor: Color,
activeTrackColor: Color
) {
if (showDialog.value) {
Dialog(onDismissRequest = { showDialog.value = false }) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(if (isDarkTheme) Color(0xFF1C1C1E).copy(alpha = 0.95f) else Color.White.copy(alpha = 0.95f), RoundedCornerShape(14.dp))
.clip(RoundedCornerShape(14.dp))
.hazeEffect(hazeState, CupertinoMaterials.regular())
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(24.dp))
Text(
title,
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = textColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 16.dp)
)
androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(12.dp))
Text(
message,
style = TextStyle(
fontSize = 14.sp,
color = textColor.copy(alpha = 0.8f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 16.dp)
)
androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(
thickness = 1.dp,
color = Color(0x40888888),
modifier = Modifier.fillMaxWidth()
)
var leftPressed by remember { mutableStateOf(false) }
var rightPressed by remember { mutableStateOf(false) }
val pressedColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
Row(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
val position = event.changes.first().position
val width = size.width.toFloat()
val height = size.height.toFloat()
val isWithinBounds = position.y >= 0 && position.y <= height
val isLeft = position.x < width / 2
event.changes.first().consume()
when (event.type) {
PointerEventType.Press -> {
if (isWithinBounds) {
leftPressed = isLeft
rightPressed = !isLeft
} else {
leftPressed = false
rightPressed = false
}
}
PointerEventType.Move -> {
if (isWithinBounds) {
leftPressed = isLeft
rightPressed = !isLeft
} else {
leftPressed = false
rightPressed = false
}
}
PointerEventType.Release -> {
if (isWithinBounds) {
if (leftPressed) {
onDismiss()
} else if (rightPressed) {
onConfirm()
}
}
leftPressed = false
rightPressed = false
}
}
}
}
},
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(if (leftPressed) pressedColor else Color.Transparent),
contentAlignment = Alignment.Center
) {
Text(dismissText, color = activeTrackColor)
}
Box(
modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.background(Color(0x40888888))
)
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(if (rightPressed) pressedColor else Color.Transparent),
contentAlignment = Alignment.Center
) {
Text(confirmText, color = activeTrackColor)
}
}
}
}
}
}
}

View File

@@ -70,14 +70,10 @@ fun LoudSoundReductionSwitch() {
for (attempt in 1..3) { for (attempt in 1..3) {
try { try {
val data = attManager.read(ATTHandles.LOUD_SOUND_REDUCTION) val data = attManager.read(ATTHandles.LOUD_SOUND_REDUCTION)
if (data.size == 2) { loudSoundReductionEnabled = data[0].toInt() != 0
loudSoundReductionEnabled = data[1].toInt() != 0 Log.d("LoudSoundReduction", "Read attempt $attempt: enabled=${loudSoundReductionEnabled}")
Log.d("LoudSoundReduction", "Read attempt $attempt: enabled=${loudSoundReductionEnabled}") parsed = true
parsed = true break
break
} else {
Log.d("LoudSoundReduction", "Read attempt $attempt returned empty data")
}
} catch (e: Exception) { } catch (e: Exception) {
Log.w("LoudSoundReduction", "Read attempt $attempt failed: ${e.message}") Log.w("LoudSoundReduction", "Read attempt $attempt failed: ${e.message}")
} }

View File

@@ -106,6 +106,9 @@ import me.kavishdevar.librepods.utils.ATTManager
import me.kavishdevar.librepods.utils.ATTHandles 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 me.kavishdevar.librepods.utils.TransparencySettings
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
import me.kavishdevar.librepods.utils.sendTransparencySettings
import java.io.IOException import java.io.IOException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
@@ -1136,182 +1139,6 @@ fun AccessibilityToggle(text: String, mutableState: MutableState<Boolean>, indep
} }
} }
private data class TransparencySettings (
val enabled: Boolean,
val leftEQ: FloatArray,
val rightEQ: FloatArray,
val leftAmplification: Float,
val rightAmplification: Float,
val leftTone: Float,
val rightTone: Float,
val leftConversationBoost: Boolean,
val rightConversationBoost: Boolean,
val leftAmbientNoiseReduction: Float,
val rightAmbientNoiseReduction: Float,
val netAmplification: Float,
val balance: Float
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TransparencySettings
if (enabled != other.enabled) return false
if (leftAmplification != other.leftAmplification) return false
if (rightAmplification != other.rightAmplification) return false
if (leftTone != other.leftTone) return false
if (rightTone != other.rightTone) return false
if (leftConversationBoost != other.leftConversationBoost) return false
if (rightConversationBoost != other.rightConversationBoost) return false
if (leftAmbientNoiseReduction != other.leftAmbientNoiseReduction) return false
if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false
if (!leftEQ.contentEquals(other.leftEQ)) return false
if (!rightEQ.contentEquals(other.rightEQ)) return false
return true
}
override fun hashCode(): Int {
var result = enabled.hashCode()
result = 31 * result + leftAmplification.hashCode()
result = 31 * result + rightAmplification.hashCode()
result = 31 * result + leftTone.hashCode()
result = 31 * result + rightTone.hashCode()
result = 31 * result + leftConversationBoost.hashCode()
result = 31 * result + rightConversationBoost.hashCode()
result = 31 * result + leftAmbientNoiseReduction.hashCode()
result = 31 * result + rightAmbientNoiseReduction.hashCode()
result = 31 * result + leftEQ.contentHashCode()
result = 31 * result + rightEQ.contentHashCode()
return result
}
}
private fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings? {
val settingsData = data.copyOfRange(1, data.size)
val buffer = ByteBuffer.wrap(settingsData).order(ByteOrder.LITTLE_ENDIAN)
val enabled = buffer.float
Log.d(TAG, "Parsed enabled: $enabled")
val leftEQ = FloatArray(8)
for (i in 0..7) {
leftEQ[i] = buffer.float
Log.d(TAG, "Parsed left EQ${i+1}: ${leftEQ[i]}")
}
val leftAmplification = buffer.float
Log.d(TAG, "Parsed left amplification: $leftAmplification")
val leftTone = buffer.float
Log.d(TAG, "Parsed left tone: $leftTone")
val leftConvFloat = buffer.float
val leftConversationBoost = leftConvFloat > 0.5f
Log.d(TAG, "Parsed left conversation boost: $leftConvFloat ($leftConversationBoost)")
val leftAmbientNoiseReduction = buffer.float
Log.d(TAG, "Parsed left ambient noise reduction: $leftAmbientNoiseReduction")
val rightEQ = FloatArray(8)
for (i in 0..7) {
rightEQ[i] = buffer.float
Log.d(TAG, "Parsed right EQ${i+1}: ${rightEQ[i]}")
}
val rightAmplification = buffer.float
Log.d(TAG, "Parsed right amplification: $rightAmplification")
val rightTone = buffer.float
Log.d(TAG, "Parsed right tone: $rightTone")
val rightConvFloat = buffer.float
val rightConversationBoost = rightConvFloat > 0.5f
Log.d(TAG, "Parsed right conversation boost: $rightConvFloat ($rightConversationBoost)")
val rightAmbientNoiseReduction = buffer.float
Log.d(TAG, "Parsed right ambient noise reduction: $rightAmbientNoiseReduction")
Log.d(TAG, "Settings parsed successfully")
val avg = (leftAmplification + rightAmplification) / 2
val amplification = avg.coerceIn(-1f, 1f)
val diff = rightAmplification - leftAmplification
val balance = diff.coerceIn(-1f, 1f)
return TransparencySettings(
enabled = enabled > 0.5f,
leftEQ = leftEQ,
rightEQ = rightEQ,
leftAmplification = leftAmplification,
rightAmplification = rightAmplification,
leftTone = leftTone,
rightTone = rightTone,
leftConversationBoost = leftConversationBoost,
rightConversationBoost = rightConversationBoost,
leftAmbientNoiseReduction = leftAmbientNoiseReduction,
rightAmbientNoiseReduction = rightAmbientNoiseReduction,
netAmplification = amplification,
balance = balance
)
}
private fun sendTransparencySettings(
attManager: ATTManager,
transparencySettings: TransparencySettings
) {
debounceJob?.cancel()
debounceJob = CoroutineScope(Dispatchers.IO).launch {
delay(100)
try {
val buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN)
Log.d(TAG,
"Sending settings: $transparencySettings"
)
buffer.putFloat(if (transparencySettings.enabled) 1.0f else 0.0f)
for (eq in transparencySettings.leftEQ) {
buffer.putFloat(eq)
}
buffer.putFloat(transparencySettings.leftAmplification)
buffer.putFloat(transparencySettings.leftTone)
buffer.putFloat(if (transparencySettings.leftConversationBoost) 1.0f else 0.0f)
buffer.putFloat(transparencySettings.leftAmbientNoiseReduction)
for (eq in transparencySettings.rightEQ) {
buffer.putFloat(eq)
}
buffer.putFloat(transparencySettings.rightAmplification)
buffer.putFloat(transparencySettings.rightTone)
buffer.putFloat(if (transparencySettings.rightConversationBoost) 1.0f else 0.0f)
buffer.putFloat(transparencySettings.rightAmbientNoiseReduction)
val data = buffer.array()
attManager.write(
ATTHandles.TRANSPARENCY,
value = data
)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
// Debounced send helper for phone/media EQ (if needed elsewhere)
private fun sendPhoneMediaEQ(aacpManager: me.kavishdevar.librepods.utils.AACPManager?, eq: FloatArray, phoneEnabled: Boolean, mediaEnabled: Boolean) {
phoneMediaDebounceJob?.cancel()
phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch {
delay(100)
try {
if (aacpManager == null) {
Log.w(TAG, "AACPManger is null; cannot send phone/media EQ")
return@launch
}
val phoneByte = if (phoneEnabled) 0x01.toByte() else 0x02.toByte()
val mediaByte = if (mediaEnabled) 0x01.toByte() else 0x02.toByte()
aacpManager.sendPhoneMediaEQ(eq, phoneByte, mediaByte)
} catch (e: Exception) {
Log.w(TAG, "Error in sendPhoneMediaEQ: ${e.message}")
}
}
}
private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.05f): Float { private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.05f): Float {
val nearest = points.minByOrNull { kotlin.math.abs(it - value) } ?: value val nearest = points.minByOrNull { kotlin.math.abs(it - value) } ?: value
return if (kotlin.math.abs(nearest - value) <= threshold) nearest else value return if (kotlin.math.abs(nearest - value) <= threshold) nearest else value
@@ -1369,3 +1196,22 @@ private fun DropdownMenuComponent(
} }
} }
} }
// Debounced send helper for phone/media EQ (if needed elsewhere)
private fun sendPhoneMediaEQ(aacpManager: me.kavishdevar.librepods.utils.AACPManager?, eq: FloatArray, phoneEnabled: Boolean, mediaEnabled: Boolean) {
phoneMediaDebounceJob?.cancel()
phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch {
delay(100)
try {
if (aacpManager == null) {
Log.w(TAG, "AACPManger is null; cannot send phone/media EQ")
return@launch
}
val phoneByte = if (phoneEnabled) 0x01.toByte() else 0x02.toByte()
val mediaByte = if (mediaEnabled) 0x01.toByte() else 0x02.toByte()
aacpManager.sendPhoneMediaEQ(eq, phoneByte, mediaByte)
} catch (e: Exception) {
Log.w(TAG, "Error in sendPhoneMediaEQ: ${e.message}")
}
}
}

View File

@@ -115,7 +115,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi
private var debounceJob: Job? = null private var debounceJob: Job? = null
private var phoneMediaDebounceJob: Job? = null private var phoneMediaDebounceJob: Job? = null
private const val TAG = "AccessibilitySettings" private const val TAG = "HearingAidAdjustments"
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
@ExperimentalHazeMaterialsApi @ExperimentalHazeMaterialsApi
@@ -201,6 +201,7 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
val ambientNoiseReductionSliderValue = remember { mutableFloatStateOf(0.0f) } val ambientNoiseReductionSliderValue = remember { mutableFloatStateOf(0.0f) }
val conversationBoostEnabled = remember { mutableStateOf(false) } val conversationBoostEnabled = remember { mutableStateOf(false) }
val eq = remember { mutableStateOf(FloatArray(8)) } val eq = remember { mutableStateOf(FloatArray(8)) }
val ownVoiceAmplification = remember { mutableFloatStateOf(0.5f) }
val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) } val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
val phoneEQEnabled = remember { mutableStateOf(false) } val phoneEQEnabled = remember { mutableStateOf(false) }
@@ -214,7 +215,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
val HearingAidSettings = remember { val HearingAidSettings = remember {
mutableStateOf( mutableStateOf(
HearingAidSettings( HearingAidSettings(
enabled = enabled.value,
leftEQ = eq.value, leftEQ = eq.value,
rightEQ = eq.value, rightEQ = eq.value,
leftAmplification = amplificationSliderValue.floatValue + (0.5f - balanceSliderValue.floatValue) * amplificationSliderValue.floatValue * 2, leftAmplification = amplificationSliderValue.floatValue + (0.5f - balanceSliderValue.floatValue) * amplificationSliderValue.floatValue * 2,
@@ -226,7 +226,8 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
netAmplification = amplificationSliderValue.floatValue, netAmplification = amplificationSliderValue.floatValue,
balance = balanceSliderValue.floatValue balance = balanceSliderValue.floatValue,
ownVoiceAmplification = ownVoiceAmplification.floatValue
) )
) )
} }
@@ -250,6 +251,26 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
} }
} }
val hearingAidATTListener = remember {
object : (ByteArray) -> Unit {
override fun invoke(value: ByteArray) {
val parsed = parseHearingAidSettingsResponse(value)
if (parsed != null) {
amplificationSliderValue.floatValue = parsed.netAmplification
balanceSliderValue.floatValue = parsed.balance
toneSliderValue.floatValue = parsed.leftTone
ambientNoiseReductionSliderValue.floatValue = parsed.leftAmbientNoiseReduction
conversationBoostEnabled.value = parsed.leftConversationBoost
eq.value = parsed.leftEQ.copyOf()
ownVoiceAmplification.floatValue = parsed.ownVoiceAmplification
Log.d(TAG, "Updated hearing aid settings from notification")
} else {
Log.w(TAG, "Failed to parse hearing aid settings from notification")
}
}
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener) aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener) aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
@@ -259,10 +280,11 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
onDispose { onDispose {
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener) aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener) aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
attManager.unregisterListener(ATTHandles.HEARING_AID, hearingAidATTListener)
} }
} }
LaunchedEffect(enabled.value, amplificationSliderValue.floatValue, balanceSliderValue.floatValue, toneSliderValue.floatValue, conversationBoostEnabled.value, ambientNoiseReductionSliderValue.floatValue, eq.value, initialLoadComplete.value, initialReadSucceeded.value) { LaunchedEffect(amplificationSliderValue.floatValue, balanceSliderValue.floatValue, toneSliderValue.floatValue, conversationBoostEnabled.value, ambientNoiseReductionSliderValue.floatValue, ownVoiceAmplification.floatValue, initialLoadComplete.value, initialReadSucceeded.value) {
if (!initialLoadComplete.value) { if (!initialLoadComplete.value) {
Log.d(TAG, "Initial device load not complete - skipping send") Log.d(TAG, "Initial device load not complete - skipping send")
return@LaunchedEffect return@LaunchedEffect
@@ -274,7 +296,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
} }
HearingAidSettings.value = HearingAidSettings( HearingAidSettings.value = HearingAidSettings(
enabled = enabled.value,
leftEQ = eq.value, leftEQ = eq.value,
rightEQ = eq.value, rightEQ = eq.value,
leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f, leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f,
@@ -286,23 +307,18 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue, rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
netAmplification = amplificationSliderValue.floatValue, netAmplification = amplificationSliderValue.floatValue,
balance = balanceSliderValue.floatValue balance = balanceSliderValue.floatValue,
ownVoiceAmplification = ownVoiceAmplification.floatValue
) )
Log.d("HearingAidSettings", "Updated settings: ${HearingAidSettings.value}") Log.d(TAG, "Updated settings: ${HearingAidSettings.value}")
// sendHearingAidSettings(attManager, HearingAidSettings.value) sendHearingAidSettings(attManager, HearingAidSettings.value)
}
DisposableEffect(Unit) {
onDispose {
// attManager.unregisterListener(ATTHandles.TRANSPARENCY, transparencyListener)
}
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
Log.d(TAG, "Connecting to ATT...") Log.d(TAG, "Connecting to ATT...")
try { try {
// attManager.enableNotifications(ATTHandles.TRANSPARENCY) attManager.enableNotifications(ATTHandles.HEARING_AID)
// attManager.registerListener(ATTHandles.TRANSPARENCY, transparencyListener) attManager.registerListener(ATTHandles.HEARING_AID, hearingAidATTListener)
try { try {
if (aacpManager != null) { if (aacpManager != null) {
@@ -324,12 +340,11 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
Log.w(TAG, "Error reading EQ from AACPManager: ${e.message}") Log.w(TAG, "Error reading EQ from AACPManager: ${e.message}")
} }
/*
var parsedSettings: HearingAidSettings? = null var parsedSettings: HearingAidSettings? = null
for (attempt in 1..3) { for (attempt in 1..3) {
initialReadAttempts.value = attempt initialReadAttempts.value = attempt
try { try {
val data = attManager.read(ATTHandles.TRANSPARENCY) val data = attManager.read(ATTHandles.HEARING_AID)
parsedSettings = parseHearingAidSettingsResponse(data = data) parsedSettings = parseHearingAidSettingsResponse(data = data)
if (parsedSettings != null) { if (parsedSettings != null) {
Log.d(TAG, "Parsed settings on attempt $attempt") Log.d(TAG, "Parsed settings on attempt $attempt")
@@ -344,19 +359,18 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
} }
if (parsedSettings != null) { if (parsedSettings != null) {
Log.d(TAG, "Initial transparency settings: $parsedSettings") Log.d(TAG, "Initial hearing aid settings: $parsedSettings")
enabled.value = parsedSettings.enabled
amplificationSliderValue.floatValue = parsedSettings.netAmplification amplificationSliderValue.floatValue = parsedSettings.netAmplification
balanceSliderValue.floatValue = parsedSettings.balance balanceSliderValue.floatValue = parsedSettings.balance
toneSliderValue.floatValue = parsedSettings.leftTone toneSliderValue.floatValue = parsedSettings.leftTone
ambientNoiseReductionSliderValue.floatValue = parsedSettings.leftAmbientNoiseReduction ambientNoiseReductionSliderValue.floatValue = parsedSettings.leftAmbientNoiseReduction
conversationBoostEnabled.value = parsedSettings.leftConversationBoost conversationBoostEnabled.value = parsedSettings.leftConversationBoost
eq.value = parsedSettings.leftEQ.copyOf() eq.value = parsedSettings.leftEQ.copyOf()
ownVoiceAmplification.floatValue = parsedSettings.ownVoiceAmplification
initialReadSucceeded.value = true initialReadSucceeded.value = true
} else { } else {
Log.d(TAG, "Failed to read/parse initial transparency settings after ${initialReadAttempts.value} attempts") Log.d(TAG, "Failed to read/parse initial hearing aid settings after ${initialReadAttempts.value} attempts")
} }
*/
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() e.printStackTrace()
} finally { } finally {
@@ -364,26 +378,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
} }
} }
LaunchedEffect(phoneMediaEQ.value, phoneEQEnabled.value, mediaEQEnabled.value) {
phoneMediaDebounceJob?.cancel()
phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch {
delay(150)
val manager = ServiceManager.getService()?.aacpManager
if (manager == null) {
Log.w(TAG, "Cannot write EQ: AACPManager not available")
return@launch
}
try {
val phoneByte = if (phoneEQEnabled.value) 0x01.toByte() else 0x02.toByte()
val mediaByte = if (mediaEQEnabled.value) 0x01.toByte() else 0x02.toByte()
Log.d(TAG, "Sending phone/media EQ (phoneEnabled=${phoneEQEnabled.value}, mediaEnabled=${mediaEQEnabled.value})")
manager.sendPhoneMediaEQ(phoneMediaEQ.value, phoneByte, mediaByte)
} catch (e: Exception) {
Log.w(TAG, "Error sending phone/media EQ: ${e.message}")
}
}
}
val isDarkThemeLocal = isSystemInDarkTheme() val isDarkThemeLocal = isSystemInDarkTheme()
var backgroundColorHA by remember { mutableStateOf(if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) } var backgroundColorHA by remember { mutableStateOf(if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
val animatedBackgroundColorHA by animateColorAsState(targetValue = backgroundColorHA, animationSpec = tween(durationMillis = 500)) val animatedBackgroundColorHA by animateColorAsState(targetValue = backgroundColorHA, animationSpec = tween(durationMillis = 500))
@@ -577,7 +571,7 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp), .padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( Text(
text = stringResource(R.string.less), text = stringResource(R.string.less),
@@ -619,7 +613,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
} }
private data class HearingAidSettings( private data class HearingAidSettings(
val enabled: Boolean,
val leftEQ: FloatArray, val leftEQ: FloatArray,
val rightEQ: FloatArray, val rightEQ: FloatArray,
val leftAmplification: Float, val leftAmplification: Float,
@@ -631,7 +624,8 @@ private data class HearingAidSettings(
val leftAmbientNoiseReduction: Float, val leftAmbientNoiseReduction: Float,
val rightAmbientNoiseReduction: Float, val rightAmbientNoiseReduction: Float,
val netAmplification: Float, val netAmplification: Float,
val balance: Float val balance: Float,
val ownVoiceAmplification: Float
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@@ -639,7 +633,6 @@ private data class HearingAidSettings(
other as HearingAidSettings other as HearingAidSettings
if (enabled != other.enabled) return false
if (leftAmplification != other.leftAmplification) return false if (leftAmplification != other.leftAmplification) return false
if (rightAmplification != other.rightAmplification) return false if (rightAmplification != other.rightAmplification) return false
if (leftTone != other.leftTone) return false if (leftTone != other.leftTone) return false
@@ -650,13 +643,13 @@ private data class HearingAidSettings(
if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false
if (!leftEQ.contentEquals(other.leftEQ)) return false if (!leftEQ.contentEquals(other.leftEQ)) return false
if (!rightEQ.contentEquals(other.rightEQ)) return false if (!rightEQ.contentEquals(other.rightEQ)) return false
if (ownVoiceAmplification != other.ownVoiceAmplification) return false
return true return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = enabled.hashCode() var result = leftAmplification.hashCode()
result = 31 * result + leftAmplification.hashCode()
result = 31 * result + rightAmplification.hashCode() result = 31 * result + rightAmplification.hashCode()
result = 31 * result + leftTone.hashCode() result = 31 * result + leftTone.hashCode()
result = 31 * result + rightTone.hashCode() result = 31 * result + rightTone.hashCode()
@@ -666,49 +659,40 @@ private data class HearingAidSettings(
result = 31 * result + rightAmbientNoiseReduction.hashCode() result = 31 * result + rightAmbientNoiseReduction.hashCode()
result = 31 * result + leftEQ.contentHashCode() result = 31 * result + leftEQ.contentHashCode()
result = 31 * result + rightEQ.contentHashCode() result = 31 * result + rightEQ.contentHashCode()
result = 31 * result + ownVoiceAmplification.hashCode()
return result return result
} }
} }
private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings? { private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings? {
val settingsData = data.copyOfRange(1, data.size) if (data.size < 104) return null
val buffer = ByteBuffer.wrap(settingsData).order(ByteOrder.LITTLE_ENDIAN) val buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
val enabled = buffer.float val phoneEnabled = buffer.get() == 0x01.toByte()
Log.d(TAG, "Parsed enabled: $enabled") val mediaEnabled = buffer.get() == 0x01.toByte()
buffer.getShort() // skip 0x60 0x00
val leftEQ = FloatArray(8) val leftEQ = FloatArray(8)
for (i in 0..7) { for (i in 0..7) {
leftEQ[i] = buffer.float leftEQ[i] = buffer.float
Log.d(TAG, "Parsed left EQ${i+1}: ${leftEQ[i]}")
} }
val leftAmplification = buffer.float val leftAmplification = buffer.float
Log.d(TAG, "Parsed left amplification: $leftAmplification")
val leftTone = buffer.float val leftTone = buffer.float
Log.d(TAG, "Parsed left tone: $leftTone")
val leftConvFloat = buffer.float val leftConvFloat = buffer.float
val leftConversationBoost = leftConvFloat > 0.5f val leftConversationBoost = leftConvFloat > 0.5f
Log.d(TAG, "Parsed left conversation boost: $leftConvFloat ($leftConversationBoost)")
val leftAmbientNoiseReduction = buffer.float val leftAmbientNoiseReduction = buffer.float
Log.d(TAG, "Parsed left ambient noise reduction: $leftAmbientNoiseReduction")
val rightEQ = FloatArray(8) val rightEQ = FloatArray(8)
for (i in 0..7) { for (i in 0..7) {
rightEQ[i] = buffer.float rightEQ[i] = buffer.float
Log.d(TAG, "Parsed right EQ${i+1}: $rightEQ[i]")
} }
val rightAmplification = buffer.float val rightAmplification = buffer.float
Log.d(TAG, "Parsed right amplification: $rightAmplification")
val rightTone = buffer.float val rightTone = buffer.float
Log.d(TAG, "Parsed right tone: $rightTone")
val rightConvFloat = buffer.float val rightConvFloat = buffer.float
val rightConversationBoost = rightConvFloat > 0.5f val rightConversationBoost = rightConvFloat > 0.5f
Log.d(TAG, "Parsed right conversation boost: $rightConvFloat ($rightConversationBoost)")
val rightAmbientNoiseReduction = buffer.float val rightAmbientNoiseReduction = buffer.float
Log.d(TAG, "Parsed right ambient noise reduction: $rightAmbientNoiseReduction")
Log.d(TAG, "Settings parsed successfully") val ownVoiceAmplification = buffer.float
val avg = (leftAmplification + rightAmplification) / 2 val avg = (leftAmplification + rightAmplification) / 2
val amplification = avg.coerceIn(-1f, 1f) val amplification = avg.coerceIn(-1f, 1f)
@@ -716,7 +700,6 @@ private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings
val balance = diff.coerceIn(-1f, 1f) val balance = diff.coerceIn(-1f, 1f)
return HearingAidSettings( return HearingAidSettings(
enabled = enabled > 0.5f,
leftEQ = leftEQ, leftEQ = leftEQ,
rightEQ = rightEQ, rightEQ = rightEQ,
leftAmplification = leftAmplification, leftAmplification = leftAmplification,
@@ -728,7 +711,8 @@ private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings
leftAmbientNoiseReduction = leftAmbientNoiseReduction, leftAmbientNoiseReduction = leftAmbientNoiseReduction,
rightAmbientNoiseReduction = rightAmbientNoiseReduction, rightAmbientNoiseReduction = rightAmbientNoiseReduction,
netAmplification = amplification, netAmplification = amplification,
balance = balance balance = balance,
ownVoiceAmplification = ownVoiceAmplification
) )
} }
@@ -740,55 +724,37 @@ private fun sendHearingAidSettings(
debounceJob = CoroutineScope(Dispatchers.IO).launch { debounceJob = CoroutineScope(Dispatchers.IO).launch {
delay(100) delay(100)
try { try {
val buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN) val currentData = attManager.read(ATTHandles.HEARING_AID)
Log.d(TAG, "Current data before update: ${currentData.joinToString(" ") { String.format("%02X", it) }}")
Log.d(TAG, if (currentData.size < 104) {
"Sending settings: $HearingAidSettings" Log.w(TAG, "Current data size ${currentData.size} too small, cannot send settings")
)
buffer.putFloat(if (HearingAidSettings.enabled) 1.0f else 0.0f)
for (eq in HearingAidSettings.leftEQ) {
buffer.putFloat(eq)
}
buffer.putFloat(HearingAidSettings.leftAmplification)
buffer.putFloat(HearingAidSettings.leftTone)
buffer.putFloat(if (HearingAidSettings.leftConversationBoost) 1.0f else 0.0f)
buffer.putFloat(HearingAidSettings.leftAmbientNoiseReduction)
for (eq in HearingAidSettings.rightEQ) {
buffer.putFloat(eq)
}
buffer.putFloat(HearingAidSettings.rightAmplification)
buffer.putFloat(HearingAidSettings.rightTone)
buffer.putFloat(if (HearingAidSettings.rightConversationBoost) 1.0f else 0.0f)
buffer.putFloat(HearingAidSettings.rightAmbientNoiseReduction)
val data = buffer.array()
attManager.write(
ATTHandles.TRANSPARENCY,
value = data
)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
private fun sendPhoneMediaEQ(aacpManager: me.kavishdevar.librepods.utils.AACPManager?, eq: FloatArray, phoneEnabled: Boolean, mediaEnabled: Boolean) {
phoneMediaDebounceJob?.cancel()
phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch {
delay(100)
try {
if (aacpManager == null) {
Log.w(TAG, "AACPManger is null; cannot send phone/media EQ")
return@launch return@launch
} }
val phoneByte = if (phoneEnabled) 0x01.toByte() else 0x02.toByte() val buffer = ByteBuffer.wrap(currentData).order(ByteOrder.LITTLE_ENDIAN)
val mediaByte = if (mediaEnabled) 0x01.toByte() else 0x02.toByte()
aacpManager.sendPhoneMediaEQ(eq, phoneByte, mediaByte) // for some reason
} catch (e: Exception) { buffer.put(2, 0x64)
Log.w(TAG, "Error in sendPhoneMediaEQ: ${e.message}")
// Left ear adjustments
buffer.putFloat(36, HearingAidSettings.leftAmplification)
buffer.putFloat(40, HearingAidSettings.leftTone)
buffer.putFloat(44, if (HearingAidSettings.leftConversationBoost) 1.0f else 0.0f)
buffer.putFloat(48, HearingAidSettings.leftAmbientNoiseReduction)
// Right ear adjustments
buffer.putFloat(84, HearingAidSettings.rightAmplification)
buffer.putFloat(88, HearingAidSettings.rightTone)
buffer.putFloat(92, if (HearingAidSettings.rightConversationBoost) 1.0f else 0.0f)
buffer.putFloat(96, HearingAidSettings.rightAmbientNoiseReduction)
// Own voice amplification
buffer.putFloat(100, HearingAidSettings.ownVoiceAmplification)
Log.d(TAG, "Sending updated settings: ${currentData.joinToString(" ") { String.format("%02X", it) }}")
attManager.write(ATTHandles.HEARING_AID, currentData)
} catch (e: IOException) {
e.printStackTrace()
} }
} }
} }

View File

@@ -59,6 +59,7 @@ import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -71,14 +72,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@@ -101,6 +103,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.composables.AccessibilitySlider import me.kavishdevar.librepods.composables.AccessibilitySlider
import me.kavishdevar.librepods.composables.ConfirmationDialog
import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch
import me.kavishdevar.librepods.composables.SinglePodANCSwitch import me.kavishdevar.librepods.composables.SinglePodANCSwitch
import me.kavishdevar.librepods.composables.StyledSwitch import me.kavishdevar.librepods.composables.StyledSwitch
@@ -111,6 +114,9 @@ import me.kavishdevar.librepods.utils.ATTManager
import me.kavishdevar.librepods.utils.ATTHandles 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 me.kavishdevar.librepods.utils.TransparencySettings
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
import me.kavishdevar.librepods.utils.sendTransparencySettings
import java.io.IOException import java.io.IOException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
@@ -143,6 +149,14 @@ fun HearingAidScreen(navController: NavController) {
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
val showDialog = remember { mutableStateOf(false) }
val hearingAidEnabled = remember {
val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }
val assistStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }
mutableStateOf((aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte()))
}
Scaffold( Scaffold(
containerColor = if (isSystemInDarkTheme()) Color( containerColor = if (isSystemInDarkTheme()) Color(
0xFF000000 0xFF000000
@@ -202,12 +216,6 @@ fun HearingAidScreen(navController: NavController) {
.verticalScroll(verticalScrollState), .verticalScroll(verticalScrollState),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
val hearingAidEnabled = remember {
val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }
val assistStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }
mutableStateOf((aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte()))
}
val hearingAidListener = remember { val hearingAidListener = remember {
object : AACPManager.ControlCommandListener { object : AACPManager.ControlCommandListener {
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) { override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
@@ -239,20 +247,20 @@ fun HearingAidScreen(navController: NavController) {
fun onChange(value: Boolean) { fun onChange(value: Boolean) {
if (value) { if (value) {
// Enable and enroll if not enrolled showDialog.value = true
val enrolled = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }?.value?.getOrNull(0) == 0x01.toByte()
if (!enrolled) {
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01)) // Enroll and enable
} else {
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01)) // Enable
}
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x01.toByte()) // Enable assist
} else { } else {
// Disable both, keep enrolled aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x02))
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x02)) // Disable aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x02.toByte())
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x02.toByte()) // Disable assist hearingAidEnabled.value = value
} }
hearingAidEnabled.value = value }
fun onAdjustPhoneChange(value: Boolean) {
adjustPhoneEnabled.value = value
}
fun onAdjustMediaChange(value: Boolean) {
adjustMediaEnabled.value = value
} }
Text( Text(
@@ -374,7 +382,7 @@ fun HearingAidScreen(navController: NavController) {
backgroundColorAdjustMedia = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) backgroundColorAdjustMedia = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
}, },
onTap = { onTap = {
adjustMediaEnabled.value = !adjustMediaEnabled.value onAdjustMediaChange(!adjustMediaEnabled.value)
} }
) )
}, },
@@ -393,7 +401,7 @@ fun HearingAidScreen(navController: NavController) {
StyledSwitch( StyledSwitch(
checked = adjustMediaEnabled.value, checked = adjustMediaEnabled.value,
onCheckedChange = { onCheckedChange = {
adjustMediaEnabled.value = it onAdjustMediaChange(it)
}, },
) )
} }
@@ -419,7 +427,7 @@ fun HearingAidScreen(navController: NavController) {
backgroundColorAdjustPhone = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) backgroundColorAdjustPhone = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
}, },
onTap = { onTap = {
adjustPhoneEnabled.value = !adjustPhoneEnabled.value onAdjustPhoneChange(!adjustPhoneEnabled.value)
} }
) )
}, },
@@ -438,11 +446,47 @@ fun HearingAidScreen(navController: NavController) {
StyledSwitch( StyledSwitch(
checked = adjustPhoneEnabled.value, checked = adjustPhoneEnabled.value,
onCheckedChange = { onCheckedChange = {
adjustPhoneEnabled.value = it onAdjustPhoneChange(it)
}, },
) )
} }
} }
} }
} }
ConfirmationDialog(
showDialog = showDialog,
title = "Enable Hearing Aid",
message = "Enabling Hearing Aid will disable Headphone Accommodation and Customized Transparency Mode.",
confirmText = "Enable",
dismissText = "Cancel",
onConfirm = {
showDialog.value = false
val enrolled = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }?.value?.getOrNull(0) == 0x01.toByte()
if (!enrolled) {
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01))
} else {
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01))
}
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x01.toByte())
hearingAidEnabled.value = true
// Disable transparency mode
CoroutineScope(Dispatchers.IO).launch {
try {
val data = attManager.read(ATTHandles.TRANSPARENCY)
val parsed = parseTransparencySettingsResponse(data)
if (parsed != null) {
val disabledSettings = parsed.copy(enabled = false)
sendTransparencySettings(attManager, disabledSettings)
}
} catch (e: Exception) {
Log.e(TAG, "Error disabling transparency: ${e.message}")
}
}
},
hazeState = hazeState,
isDarkTheme = isDarkTheme,
textColor = textColor,
activeTrackColor = activeTrackColor
)
} }

View File

@@ -39,8 +39,8 @@ import java.util.concurrent.TimeUnit
enum class ATTHandles(val value: Int) { enum class ATTHandles(val value: Int) {
TRANSPARENCY(0x18), TRANSPARENCY(0x18),
LOUD_SOUND_REDUCTION(0x1b), LOUD_SOUND_REDUCTION(0x1B),
HEARING_AID(0x2a), HEARING_AID(0x2A),
} }
enum class ATTCCCDHandles(val value: Int) { enum class ATTCCCDHandles(val value: Int) {
@@ -85,7 +85,7 @@ class ATTManager(private val device: BluetoothDevice) {
if (pdu.isNotEmpty() && pdu[0] == OPCODE_HANDLE_VALUE_NTF) { if (pdu.isNotEmpty() && pdu[0] == OPCODE_HANDLE_VALUE_NTF) {
// notification -> dispatch to listeners // notification -> dispatch to listeners
val handle = (pdu[1].toInt() and 0xFF) or ((pdu[2].toInt() and 0xFF) shl 8) val handle = (pdu[1].toInt() and 0xFF) or ((pdu[2].toInt() and 0xFF) shl 8)
val value = pdu.copyOfRange(2, pdu.size) val value = pdu.copyOfRange(3, pdu.size)
listeners[handle]?.forEach { listener -> listeners[handle]?.forEach { listener ->
try { try {
listener(value) listener(value)
@@ -191,7 +191,7 @@ class ATTManager(private val device: BluetoothDevice) {
throw IllegalStateException("No response read from ATT socket within $timeoutMs ms") throw IllegalStateException("No response read from ATT socket within $timeoutMs ms")
} }
Log.d(TAG, "readResponse: ${resp.joinToString(" ") { String.format("%02X", it) }}") Log.d(TAG, "readResponse: ${resp.joinToString(" ") { String.format("%02X", it) }}")
return resp return resp.copyOfRange(1, resp.size)
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
Thread.currentThread().interrupt() Thread.currentThread().interrupt()
throw IllegalStateException("Interrupted while waiting for ATT response", e) throw IllegalStateException("Interrupted while waiting for ATT response", e)

View File

@@ -0,0 +1,149 @@
package me.kavishdevar.librepods.utils
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrder
private const val TAG = "TransparencyUtils"
data class TransparencySettings(
val enabled: Boolean,
val leftEQ: FloatArray,
val rightEQ: FloatArray,
val leftAmplification: Float,
val rightAmplification: Float,
val leftTone: Float,
val rightTone: Float,
val leftConversationBoost: Boolean,
val rightConversationBoost: Boolean,
val leftAmbientNoiseReduction: Float,
val rightAmbientNoiseReduction: Float,
val netAmplification: Float,
val balance: Float
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TransparencySettings
if (enabled != other.enabled) return false
if (leftAmplification != other.leftAmplification) return false
if (rightAmplification != other.rightAmplification) return false
if (leftTone != other.leftTone) return false
if (rightTone != other.rightTone) return false
if (leftConversationBoost != other.leftConversationBoost) return false
if (rightConversationBoost != other.rightConversationBoost) return false
if (leftAmbientNoiseReduction != other.leftAmbientNoiseReduction) return false
if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false
if (!leftEQ.contentEquals(other.leftEQ)) return false
if (!rightEQ.contentEquals(other.rightEQ)) return false
return true
}
override fun hashCode(): Int {
var result = enabled.hashCode()
result = 31 * result + leftAmplification.hashCode()
result = 31 * result + rightAmplification.hashCode()
result = 31 * result + leftTone.hashCode()
result = 31 * result + rightTone.hashCode()
result = 31 * result + leftConversationBoost.hashCode()
result = 31 * result + rightConversationBoost.hashCode()
result = 31 * result + leftAmbientNoiseReduction.hashCode()
result = 31 * result + rightAmbientNoiseReduction.hashCode()
result = 31 * result + leftEQ.contentHashCode()
result = 31 * result + rightEQ.contentHashCode()
return result
}
}
fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings? {
val settingsData = data
val buffer = ByteBuffer.wrap(settingsData).order(ByteOrder.LITTLE_ENDIAN)
val enabled = buffer.float
val leftEQ = FloatArray(8)
for (i in 0..7) {
leftEQ[i] = buffer.float
}
val leftAmplification = buffer.float
val leftTone = buffer.float
val leftConvFloat = buffer.float
val leftConversationBoost = leftConvFloat > 0.5f
val leftAmbientNoiseReduction = buffer.float
val rightEQ = FloatArray(8)
for (i in 0..7) {
rightEQ[i] = buffer.float
}
val rightAmplification = buffer.float
val rightTone = buffer.float
val rightConvFloat = buffer.float
val rightConversationBoost = rightConvFloat > 0.5f
val rightAmbientNoiseReduction = buffer.float
val avg = (leftAmplification + rightAmplification) / 2
val amplification = avg.coerceIn(-1f, 1f)
val diff = rightAmplification - leftAmplification
val balance = diff.coerceIn(-1f, 1f)
return TransparencySettings(
enabled = enabled > 0.5f,
leftEQ = leftEQ,
rightEQ = rightEQ,
leftAmplification = leftAmplification,
rightAmplification = rightAmplification,
leftTone = leftTone,
rightTone = rightTone,
leftConversationBoost = leftConversationBoost,
rightConversationBoost = rightConversationBoost,
leftAmbientNoiseReduction = leftAmbientNoiseReduction,
rightAmbientNoiseReduction = rightAmbientNoiseReduction,
netAmplification = amplification,
balance = balance
)
}
private var debounceJob: Job? = null
fun sendTransparencySettings(attManager: ATTManager, transparencySettings: TransparencySettings) {
debounceJob?.cancel()
debounceJob = CoroutineScope(Dispatchers.IO).launch {
delay(100)
try {
val buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN)
buffer.putFloat(if (transparencySettings.enabled) 1.0f else 0.0f)
for (eq in transparencySettings.leftEQ) {
buffer.putFloat(eq)
}
buffer.putFloat(transparencySettings.leftAmplification)
buffer.putFloat(transparencySettings.leftTone)
buffer.putFloat(if (transparencySettings.leftConversationBoost) 1.0f else 0.0f)
buffer.putFloat(transparencySettings.leftAmbientNoiseReduction)
for (eq in transparencySettings.rightEQ) {
buffer.putFloat(eq)
}
buffer.putFloat(transparencySettings.rightAmplification)
buffer.putFloat(transparencySettings.rightTone)
buffer.putFloat(if (transparencySettings.rightConversationBoost) 1.0f else 0.0f)
buffer.putFloat(transparencySettings.rightAmbientNoiseReduction)
val data = buffer.array()
attManager.write(ATTHandles.TRANSPARENCY, value = data)
} catch (e: IOException) {
e.printStackTrace()
}
}
}