mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-01 07:39:11 +00:00
android: add hearing aid adjustments
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,14 +70,10 @@ fun LoudSoundReductionSwitch() {
|
||||
for (attempt in 1..3) {
|
||||
try {
|
||||
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}")
|
||||
parsed = true
|
||||
break
|
||||
} else {
|
||||
Log.d("LoudSoundReduction", "Read attempt $attempt returned empty data")
|
||||
}
|
||||
loudSoundReductionEnabled = data[0].toInt() != 0
|
||||
Log.d("LoudSoundReduction", "Read attempt $attempt: enabled=${loudSoundReductionEnabled}")
|
||||
parsed = true
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
Log.w("LoudSoundReduction", "Read attempt $attempt failed: ${e.message}")
|
||||
}
|
||||
|
||||
@@ -106,6 +106,9 @@ 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 me.kavishdevar.librepods.utils.TransparencySettings
|
||||
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
|
||||
import me.kavishdevar.librepods.utils.sendTransparencySettings
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
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 {
|
||||
val nearest = points.minByOrNull { kotlin.math.abs(it - value) } ?: 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
private var debounceJob: Job? = null
|
||||
private var phoneMediaDebounceJob: Job? = null
|
||||
private const val TAG = "AccessibilitySettings"
|
||||
private const val TAG = "HearingAidAdjustments"
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@ExperimentalHazeMaterialsApi
|
||||
@@ -201,6 +201,7 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
||||
val ambientNoiseReductionSliderValue = remember { mutableFloatStateOf(0.0f) }
|
||||
val conversationBoostEnabled = remember { mutableStateOf(false) }
|
||||
val eq = remember { mutableStateOf(FloatArray(8)) }
|
||||
val ownVoiceAmplification = remember { mutableFloatStateOf(0.5f) }
|
||||
|
||||
val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
|
||||
val phoneEQEnabled = remember { mutableStateOf(false) }
|
||||
@@ -214,7 +215,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
||||
val HearingAidSettings = remember {
|
||||
mutableStateOf(
|
||||
HearingAidSettings(
|
||||
enabled = enabled.value,
|
||||
leftEQ = eq.value,
|
||||
rightEQ = eq.value,
|
||||
leftAmplification = amplificationSliderValue.floatValue + (0.5f - balanceSliderValue.floatValue) * amplificationSliderValue.floatValue * 2,
|
||||
@@ -226,7 +226,8 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
||||
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.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) {
|
||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
||||
@@ -259,10 +280,11 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
||||
onDispose {
|
||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, 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) {
|
||||
Log.d(TAG, "Initial device load not complete - skipping send")
|
||||
return@LaunchedEffect
|
||||
@@ -274,7 +296,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
||||
}
|
||||
|
||||
HearingAidSettings.value = HearingAidSettings(
|
||||
enabled = enabled.value,
|
||||
leftEQ = eq.value,
|
||||
rightEQ = eq.value,
|
||||
leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f,
|
||||
@@ -286,23 +307,18 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
||||
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||
netAmplification = amplificationSliderValue.floatValue,
|
||||
balance = balanceSliderValue.floatValue
|
||||
balance = balanceSliderValue.floatValue,
|
||||
ownVoiceAmplification = ownVoiceAmplification.floatValue
|
||||
)
|
||||
Log.d("HearingAidSettings", "Updated settings: ${HearingAidSettings.value}")
|
||||
// sendHearingAidSettings(attManager, HearingAidSettings.value)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
// attManager.unregisterListener(ATTHandles.TRANSPARENCY, transparencyListener)
|
||||
}
|
||||
Log.d(TAG, "Updated settings: ${HearingAidSettings.value}")
|
||||
sendHearingAidSettings(attManager, HearingAidSettings.value)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
Log.d(TAG, "Connecting to ATT...")
|
||||
try {
|
||||
// attManager.enableNotifications(ATTHandles.TRANSPARENCY)
|
||||
// attManager.registerListener(ATTHandles.TRANSPARENCY, transparencyListener)
|
||||
attManager.enableNotifications(ATTHandles.HEARING_AID)
|
||||
attManager.registerListener(ATTHandles.HEARING_AID, hearingAidATTListener)
|
||||
|
||||
try {
|
||||
if (aacpManager != null) {
|
||||
@@ -324,12 +340,11 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
||||
Log.w(TAG, "Error reading EQ from AACPManager: ${e.message}")
|
||||
}
|
||||
|
||||
/*
|
||||
var parsedSettings: HearingAidSettings? = null
|
||||
for (attempt in 1..3) {
|
||||
initialReadAttempts.value = attempt
|
||||
try {
|
||||
val data = attManager.read(ATTHandles.TRANSPARENCY)
|
||||
val data = attManager.read(ATTHandles.HEARING_AID)
|
||||
parsedSettings = parseHearingAidSettingsResponse(data = data)
|
||||
if (parsedSettings != null) {
|
||||
Log.d(TAG, "Parsed settings on attempt $attempt")
|
||||
@@ -344,19 +359,18 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
||||
}
|
||||
|
||||
if (parsedSettings != null) {
|
||||
Log.d(TAG, "Initial transparency settings: $parsedSettings")
|
||||
enabled.value = parsedSettings.enabled
|
||||
Log.d(TAG, "Initial hearing aid settings: $parsedSettings")
|
||||
amplificationSliderValue.floatValue = parsedSettings.netAmplification
|
||||
balanceSliderValue.floatValue = parsedSettings.balance
|
||||
toneSliderValue.floatValue = parsedSettings.leftTone
|
||||
ambientNoiseReductionSliderValue.floatValue = parsedSettings.leftAmbientNoiseReduction
|
||||
conversationBoostEnabled.value = parsedSettings.leftConversationBoost
|
||||
eq.value = parsedSettings.leftEQ.copyOf()
|
||||
ownVoiceAmplification.floatValue = parsedSettings.ownVoiceAmplification
|
||||
initialReadSucceeded.value = true
|
||||
} 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) {
|
||||
e.printStackTrace()
|
||||
} 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()
|
||||
var backgroundColorHA by remember { mutableStateOf(if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||
val animatedBackgroundColorHA by animateColorAsState(targetValue = backgroundColorHA, animationSpec = tween(durationMillis = 500))
|
||||
@@ -577,7 +571,7 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.less),
|
||||
@@ -619,7 +613,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
||||
}
|
||||
|
||||
private data class HearingAidSettings(
|
||||
val enabled: Boolean,
|
||||
val leftEQ: FloatArray,
|
||||
val rightEQ: FloatArray,
|
||||
val leftAmplification: Float,
|
||||
@@ -631,7 +624,8 @@ private data class HearingAidSettings(
|
||||
val leftAmbientNoiseReduction: Float,
|
||||
val rightAmbientNoiseReduction: Float,
|
||||
val netAmplification: Float,
|
||||
val balance: Float
|
||||
val balance: Float,
|
||||
val ownVoiceAmplification: Float
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
@@ -639,7 +633,6 @@ private data class HearingAidSettings(
|
||||
|
||||
other as HearingAidSettings
|
||||
|
||||
if (enabled != other.enabled) return false
|
||||
if (leftAmplification != other.leftAmplification) return false
|
||||
if (rightAmplification != other.rightAmplification) return false
|
||||
if (leftTone != other.leftTone) return false
|
||||
@@ -650,13 +643,13 @@ private data class HearingAidSettings(
|
||||
if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false
|
||||
if (!leftEQ.contentEquals(other.leftEQ)) return false
|
||||
if (!rightEQ.contentEquals(other.rightEQ)) return false
|
||||
if (ownVoiceAmplification != other.ownVoiceAmplification) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = enabled.hashCode()
|
||||
result = 31 * result + leftAmplification.hashCode()
|
||||
var result = leftAmplification.hashCode()
|
||||
result = 31 * result + rightAmplification.hashCode()
|
||||
result = 31 * result + leftTone.hashCode()
|
||||
result = 31 * result + rightTone.hashCode()
|
||||
@@ -666,49 +659,40 @@ private data class HearingAidSettings(
|
||||
result = 31 * result + rightAmbientNoiseReduction.hashCode()
|
||||
result = 31 * result + leftEQ.contentHashCode()
|
||||
result = 31 * result + rightEQ.contentHashCode()
|
||||
result = 31 * result + ownVoiceAmplification.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings? {
|
||||
val settingsData = data.copyOfRange(1, data.size)
|
||||
val buffer = ByteBuffer.wrap(settingsData).order(ByteOrder.LITTLE_ENDIAN)
|
||||
if (data.size < 104) return null
|
||||
val buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
|
||||
|
||||
val enabled = buffer.float
|
||||
Log.d(TAG, "Parsed enabled: $enabled")
|
||||
val phoneEnabled = buffer.get() == 0x01.toByte()
|
||||
val mediaEnabled = buffer.get() == 0x01.toByte()
|
||||
buffer.getShort() // skip 0x60 0x00
|
||||
|
||||
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 ownVoiceAmplification = buffer.float
|
||||
|
||||
val avg = (leftAmplification + rightAmplification) / 2
|
||||
val amplification = avg.coerceIn(-1f, 1f)
|
||||
@@ -716,7 +700,6 @@ private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings
|
||||
val balance = diff.coerceIn(-1f, 1f)
|
||||
|
||||
return HearingAidSettings(
|
||||
enabled = enabled > 0.5f,
|
||||
leftEQ = leftEQ,
|
||||
rightEQ = rightEQ,
|
||||
leftAmplification = leftAmplification,
|
||||
@@ -728,7 +711,8 @@ private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings
|
||||
leftAmbientNoiseReduction = leftAmbientNoiseReduction,
|
||||
rightAmbientNoiseReduction = rightAmbientNoiseReduction,
|
||||
netAmplification = amplification,
|
||||
balance = balance
|
||||
balance = balance,
|
||||
ownVoiceAmplification = ownVoiceAmplification
|
||||
)
|
||||
}
|
||||
|
||||
@@ -740,55 +724,37 @@ private fun sendHearingAidSettings(
|
||||
debounceJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
delay(100)
|
||||
try {
|
||||
val buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN)
|
||||
|
||||
Log.d(TAG,
|
||||
"Sending settings: $HearingAidSettings"
|
||||
)
|
||||
|
||||
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")
|
||||
val currentData = attManager.read(ATTHandles.HEARING_AID)
|
||||
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")
|
||||
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}")
|
||||
val buffer = ByteBuffer.wrap(currentData).order(ByteOrder.LITTLE_ENDIAN)
|
||||
|
||||
// for some reason
|
||||
buffer.put(2, 0x64)
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
@@ -71,14 +72,15 @@ 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.draw.drawBehind
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
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.PointerEventPass
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
@@ -101,6 +103,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.composables.AccessibilitySlider
|
||||
import me.kavishdevar.librepods.composables.ConfirmationDialog
|
||||
import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch
|
||||
import me.kavishdevar.librepods.composables.SinglePodANCSwitch
|
||||
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.AACPManager
|
||||
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.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
@@ -143,6 +149,14 @@ fun HearingAidScreen(navController: NavController) {
|
||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||
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(
|
||||
containerColor = if (isSystemInDarkTheme()) Color(
|
||||
0xFF000000
|
||||
@@ -202,12 +216,6 @@ fun HearingAidScreen(navController: NavController) {
|
||||
.verticalScroll(verticalScrollState),
|
||||
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 {
|
||||
object : AACPManager.ControlCommandListener {
|
||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||
@@ -239,20 +247,20 @@ fun HearingAidScreen(navController: NavController) {
|
||||
|
||||
fun onChange(value: Boolean) {
|
||||
if (value) {
|
||||
// Enable and enroll if not enrolled
|
||||
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
|
||||
showDialog.value = true
|
||||
} else {
|
||||
// Disable both, keep enrolled
|
||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x02)) // Disable
|
||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x02.toByte()) // Disable assist
|
||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x02))
|
||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x02.toByte())
|
||||
hearingAidEnabled.value = value
|
||||
}
|
||||
hearingAidEnabled.value = value
|
||||
}
|
||||
|
||||
fun onAdjustPhoneChange(value: Boolean) {
|
||||
adjustPhoneEnabled.value = value
|
||||
}
|
||||
|
||||
fun onAdjustMediaChange(value: Boolean) {
|
||||
adjustMediaEnabled.value = value
|
||||
}
|
||||
|
||||
Text(
|
||||
@@ -374,7 +382,7 @@ fun HearingAidScreen(navController: NavController) {
|
||||
backgroundColorAdjustMedia = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
},
|
||||
onTap = {
|
||||
adjustMediaEnabled.value = !adjustMediaEnabled.value
|
||||
onAdjustMediaChange(!adjustMediaEnabled.value)
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -393,7 +401,7 @@ fun HearingAidScreen(navController: NavController) {
|
||||
StyledSwitch(
|
||||
checked = adjustMediaEnabled.value,
|
||||
onCheckedChange = {
|
||||
adjustMediaEnabled.value = it
|
||||
onAdjustMediaChange(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -419,7 +427,7 @@ fun HearingAidScreen(navController: NavController) {
|
||||
backgroundColorAdjustPhone = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
},
|
||||
onTap = {
|
||||
adjustPhoneEnabled.value = !adjustPhoneEnabled.value
|
||||
onAdjustPhoneChange(!adjustPhoneEnabled.value)
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -438,11 +446,47 @@ fun HearingAidScreen(navController: NavController) {
|
||||
StyledSwitch(
|
||||
checked = adjustPhoneEnabled.value,
|
||||
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
|
||||
)
|
||||
}
|
||||
@@ -39,8 +39,8 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
enum class ATTHandles(val value: Int) {
|
||||
TRANSPARENCY(0x18),
|
||||
LOUD_SOUND_REDUCTION(0x1b),
|
||||
HEARING_AID(0x2a),
|
||||
LOUD_SOUND_REDUCTION(0x1B),
|
||||
HEARING_AID(0x2A),
|
||||
}
|
||||
|
||||
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) {
|
||||
// notification -> dispatch to listeners
|
||||
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 ->
|
||||
try {
|
||||
listener(value)
|
||||
@@ -191,7 +191,7 @@ class ATTManager(private val device: BluetoothDevice) {
|
||||
throw IllegalStateException("No response read from ATT socket within $timeoutMs ms")
|
||||
}
|
||||
Log.d(TAG, "readResponse: ${resp.joinToString(" ") { String.format("%02X", it) }}")
|
||||
return resp
|
||||
return resp.copyOfRange(1, resp.size)
|
||||
} catch (e: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
throw IllegalStateException("Interrupted while waiting for ATT response", e)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user