mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-31 23:29:10 +00:00
android: add ui for hearing stuff
mostly copied from the transparency settings, which are now updated to match ios <26 ui
This commit is contained in:
@@ -112,6 +112,8 @@ import me.kavishdevar.librepods.screens.AirPodsSettingsScreen
|
||||
import me.kavishdevar.librepods.screens.AppSettingsScreen
|
||||
import me.kavishdevar.librepods.screens.DebugScreen
|
||||
import me.kavishdevar.librepods.screens.HeadTrackingScreen
|
||||
import me.kavishdevar.librepods.screens.HearingAidScreen
|
||||
import me.kavishdevar.librepods.screens.HearingAidAdjustmentsScreen
|
||||
import me.kavishdevar.librepods.screens.LongPress
|
||||
import me.kavishdevar.librepods.screens.Onboarding
|
||||
import me.kavishdevar.librepods.screens.RenameScreen
|
||||
@@ -380,6 +382,12 @@ fun Main() {
|
||||
composable("onboarding") {
|
||||
Onboarding(navController, context)
|
||||
}
|
||||
composable("hearing_aid") {
|
||||
HearingAidScreen(navController)
|
||||
}
|
||||
composable("hearing_aid_adjustments") {
|
||||
HearingAidAdjustmentsScreen(navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,10 +49,11 @@ import kotlin.math.roundToInt
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccessibilitySlider(
|
||||
label: String,
|
||||
label: String? = null,
|
||||
value: Float,
|
||||
onValueChange: (Float) -> Unit,
|
||||
valueRange: ClosedFloatingPointRange<Float>
|
||||
valueRange: ClosedFloatingPointRange<Float>,
|
||||
widthFrac: Float = 1f
|
||||
) {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
|
||||
@@ -62,18 +63,20 @@ fun AccessibilitySlider(
|
||||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxWidth(widthFrac),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = labelTextColor,
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily(Font(R.font.sf_pro))
|
||||
if (label != null) {
|
||||
Text(
|
||||
text = label,
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = labelTextColor,
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Slider(
|
||||
value = value,
|
||||
|
||||
@@ -27,7 +27,10 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -45,17 +48,22 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import me.kavishdevar.librepods.services.AirPodsService
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import androidx.core.content.edit
|
||||
import android.util.Log
|
||||
|
||||
@Composable
|
||||
fun IndependentToggle(name: String, service: AirPodsService? = null, functionName: String? = null, sharedPreferences: SharedPreferences, default: Boolean = false, controlCommandIdentifier: AACPManager.Companion.ControlCommandIdentifiers? = null) {
|
||||
fun IndependentToggle(name: String, service: AirPodsService? = null, functionName: String? = null, sharedPreferences: SharedPreferences, default: Boolean = false, controlCommandIdentifier: AACPManager.Companion.ControlCommandIdentifiers? = null, description: String? = null) {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val snakeCasedName =
|
||||
@@ -109,39 +117,57 @@ fun IndependentToggle(name: String, service: AirPodsService? = null, functionNam
|
||||
}
|
||||
}
|
||||
}
|
||||
Box (
|
||||
Column (
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.background(animatedBackgroundColor, RoundedCornerShape(14.dp))
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
backgroundColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||
tryAwaitRelease()
|
||||
backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
},
|
||||
onTap = {
|
||||
checked = !checked
|
||||
cb()
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
{
|
||||
Row(
|
||||
.padding(vertical = 8.dp),
|
||||
) {
|
||||
Box (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(55.dp)
|
||||
.padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = name, modifier = Modifier.weight(1f), fontSize = 16.sp, color = textColor)
|
||||
StyledSwitch(
|
||||
checked = checked,
|
||||
onCheckedChange = {
|
||||
checked = it
|
||||
cb()
|
||||
.background(animatedBackgroundColor, RoundedCornerShape(14.dp))
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
backgroundColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||
tryAwaitRelease()
|
||||
backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
},
|
||||
onTap = {
|
||||
checked = !checked
|
||||
cb()
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
{
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(55.dp)
|
||||
.padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = name, modifier = Modifier.weight(1f), fontSize = 16.sp, color = textColor)
|
||||
StyledSwitch(
|
||||
checked = checked,
|
||||
onCheckedChange = {
|
||||
checked = it
|
||||
cb()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (description != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = description,
|
||||
style = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,9 +111,9 @@ import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
var debounceJob: Job? = null
|
||||
var phoneMediaDebounceJob: Job? = null
|
||||
const val TAG = "AccessibilitySettings"
|
||||
private var debounceJob: Job? = null
|
||||
private var phoneMediaDebounceJob: Job? = null
|
||||
private const val TAG = "AccessibilitySettings"
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@ExperimentalHazeMaterialsApi
|
||||
@@ -150,7 +150,7 @@ fun AccessibilitySettingsScreen() {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Accessibility Settings",
|
||||
text = stringResource(R.string.accessibility),
|
||||
style = TextStyle(
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
@@ -452,79 +452,232 @@ fun AccessibilitySettingsScreen() {
|
||||
// Only show transparency mode section if SDP offset is available
|
||||
if (isSdpOffsetAvailable.value) {
|
||||
AccessibilityToggle(
|
||||
text = "Transparency Mode",
|
||||
text = stringResource(R.string.transparency_mode),
|
||||
mutableState = enabled,
|
||||
independent = true
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.customize_transparency_mode_description),
|
||||
style = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
color = textColor.copy(0.6f),
|
||||
lineHeight = 14.sp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
independent = true,
|
||||
description = stringResource(R.string.customize_transparency_mode_description)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Customize Transparency Mode".uppercase(),
|
||||
text = stringResource(R.string.amplification).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(8.dp)
|
||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
||||
.height(55.dp)
|
||||
) {
|
||||
AccessibilitySlider(
|
||||
label = "Amplification",
|
||||
valueRange = -1f..1f,
|
||||
value = amplificationSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
amplificationSliderValue.floatValue = snapIfClose(it, listOf(-0.5f, -0.25f, 0f, 0.25f, 0.5f))
|
||||
},
|
||||
)
|
||||
AccessibilitySlider(
|
||||
label = "Balance",
|
||||
valueRange = -1f..1f,
|
||||
value = balanceSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
balanceSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
||||
},
|
||||
)
|
||||
AccessibilitySlider(
|
||||
label = "Tone",
|
||||
valueRange = -1f..1f,
|
||||
value = toneSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
toneSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
||||
},
|
||||
)
|
||||
AccessibilitySlider(
|
||||
label = "Ambient Noise Reduction",
|
||||
valueRange = 0f..1f,
|
||||
value = ambientNoiseReductionSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
ambientNoiseReductionSliderValue.floatValue = snapIfClose(it, listOf(0.1f, 0.3f, 0.5f, 0.7f, 0.9f))
|
||||
},
|
||||
)
|
||||
AccessibilityToggle(
|
||||
text = "Conversation Boost",
|
||||
mutableState = conversationBoostEnabled
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Text(
|
||||
text = "",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(start = 4.dp)
|
||||
)
|
||||
AccessibilitySlider(
|
||||
valueRange = -1f..1f,
|
||||
value = amplificationSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
amplificationSliderValue.floatValue = snapIfClose(it, listOf(-0.5f, -0.25f, 0f, 0.25f, 0.5f))
|
||||
},
|
||||
widthFrac = 0.90f,
|
||||
)
|
||||
Text(
|
||||
text = "",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(end = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.balance).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.left),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.right),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
}
|
||||
AccessibilitySlider(
|
||||
valueRange = -1f..1f,
|
||||
value = balanceSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
balanceSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.tone).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.darker),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.brighter),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
}
|
||||
AccessibilitySlider(
|
||||
valueRange = -1f..1f,
|
||||
value = toneSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
toneSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.ambient_noise_reduction).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.less),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.more),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
}
|
||||
AccessibilitySlider(
|
||||
valueRange = 0f..1f,
|
||||
value = ambientNoiseReductionSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
ambientNoiseReductionSliderValue.floatValue = snapIfClose(it, listOf(0.1f, 0.3f, 0.5f, 0.7f, 0.9f))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AccessibilityToggle(
|
||||
text = stringResource(R.string.conversation_boost),
|
||||
mutableState = conversationBoostEnabled,
|
||||
independent = true,
|
||||
description = stringResource(R.string.conversation_boost_description)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "AUDIO",
|
||||
text = stringResource(R.string.audio).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
@@ -542,7 +695,7 @@ fun AccessibilitySettingsScreen() {
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Tone Volume",
|
||||
text = stringResource(R.string.tone_volume),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
@@ -559,8 +712,8 @@ fun AccessibilitySettingsScreen() {
|
||||
LoudSoundReductionSwitch()
|
||||
|
||||
DropdownMenuComponent(
|
||||
label = "Press Speed",
|
||||
options = pressSpeedOptions.values.toList(),
|
||||
label = stringResource(R.string.press_speed),
|
||||
options = listOf(stringResource(R.string.default_option), stringResource(R.string.slower), stringResource(R.string.slowest)),
|
||||
selectedOption = selectedPressSpeed.toString(),
|
||||
onOptionSelected = { newValue ->
|
||||
selectedPressSpeed = newValue
|
||||
@@ -572,8 +725,8 @@ fun AccessibilitySettingsScreen() {
|
||||
textColor = textColor
|
||||
)
|
||||
DropdownMenuComponent(
|
||||
label = "Press and Hold Duration",
|
||||
options = pressAndHoldDurationOptions.values.toList(),
|
||||
label = stringResource(R.string.press_and_hold_duration),
|
||||
options = listOf(stringResource(R.string.default_option), stringResource(R.string.slower), stringResource(R.string.slowest)),
|
||||
selectedOption = selectedPressAndHoldDuration.toString(),
|
||||
onOptionSelected = { newValue ->
|
||||
selectedPressAndHoldDuration = newValue
|
||||
@@ -585,8 +738,8 @@ fun AccessibilitySettingsScreen() {
|
||||
textColor = textColor
|
||||
)
|
||||
DropdownMenuComponent(
|
||||
label = "Volume Swipe Speed",
|
||||
options = volumeSwipeSpeedOptions.values.toList(),
|
||||
label = stringResource(R.string.volume_swipe_speed),
|
||||
options = listOf(stringResource(R.string.default_option), stringResource(R.string.longer), stringResource(R.string.longest)),
|
||||
selectedOption = selectedVolumeSwipeSpeed.toString(),
|
||||
onOptionSelected = { newValue ->
|
||||
selectedVolumeSwipeSpeed = newValue
|
||||
@@ -603,7 +756,7 @@ fun AccessibilitySettingsScreen() {
|
||||
// Only show transparency mode EQ section if SDP offset is available
|
||||
if (isSdpOffsetAvailable.value) {
|
||||
Text(
|
||||
text = "Equalizer".uppercase(),
|
||||
text = stringResource(R.string.equalizer).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
@@ -687,7 +840,7 @@ fun AccessibilitySettingsScreen() {
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Band ${i + 1}",
|
||||
text = stringResource(R.string.band_label, i + 1),
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
@@ -700,7 +853,7 @@ fun AccessibilitySettingsScreen() {
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Apply EQ to".uppercase(),
|
||||
text = stringResource(R.string.apply_eq_to).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
@@ -740,7 +893,7 @@ fun AccessibilitySettingsScreen() {
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
"Phone",
|
||||
stringResource(R.string.phone),
|
||||
fontSize = 16.sp,
|
||||
color = textColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
@@ -791,7 +944,7 @@ fun AccessibilitySettingsScreen() {
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
"Media",
|
||||
stringResource(R.string.media),
|
||||
fontSize = 16.sp,
|
||||
color = textColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
@@ -888,7 +1041,7 @@ fun AccessibilitySettingsScreen() {
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Band ${i + 1}",
|
||||
text = stringResource(R.string.band_label, i + 1),
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
@@ -902,57 +1055,75 @@ fun AccessibilitySettingsScreen() {
|
||||
|
||||
|
||||
@Composable
|
||||
fun AccessibilityToggle(text: String, mutableState: MutableState<Boolean>, independent: Boolean = false) {
|
||||
fun AccessibilityToggle(text: String, mutableState: MutableState<Boolean>, independent: Boolean = false, description: String? = null) {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val boxPaddings = if (independent) 2.dp else 4.dp
|
||||
val cornerShape = if (independent) RoundedCornerShape(14.dp) else RoundedCornerShape(0.dp)
|
||||
Box (
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = boxPaddings)
|
||||
.background(animatedBackgroundColor, cornerShape)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
backgroundColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||
tryAwaitRelease()
|
||||
backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
},
|
||||
onTap = {
|
||||
mutableState.value = !mutableState.value
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
{
|
||||
val rowHeight = if (independent) 55.dp else 50.dp
|
||||
val rowPadding = if (independent) 12.dp else 4.dp
|
||||
Row(
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Box (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(rowHeight)
|
||||
.padding(horizontal = rowPadding),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.weight(1f),
|
||||
fontSize = 16.sp,
|
||||
color = textColor
|
||||
)
|
||||
StyledSwitch(
|
||||
checked = mutableState.value,
|
||||
onCheckedChange = {
|
||||
mutableState.value = it
|
||||
.background(animatedBackgroundColor, cornerShape)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
backgroundColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||
tryAwaitRelease()
|
||||
backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
},
|
||||
onTap = {
|
||||
mutableState.value = !mutableState.value
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
{
|
||||
val rowHeight = if (independent) 55.dp else 50.dp
|
||||
val rowPadding = if (independent) 12.dp else 4.dp
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(rowHeight)
|
||||
.padding(horizontal = rowPadding),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.weight(1f),
|
||||
fontSize = 16.sp,
|
||||
color = textColor
|
||||
)
|
||||
StyledSwitch(
|
||||
checked = mutableState.value,
|
||||
onCheckedChange = {
|
||||
mutableState.value = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (description != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = description,
|
||||
style = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class TransparencySettings (
|
||||
private data class TransparencySettings (
|
||||
val enabled: Boolean,
|
||||
val leftEQ: FloatArray,
|
||||
val rightEQ: FloatArray,
|
||||
@@ -1134,7 +1305,7 @@ private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DropdownMenuComponent(
|
||||
private fun DropdownMenuComponent(
|
||||
label: String,
|
||||
options: List<String>,
|
||||
selectedOption: String,
|
||||
|
||||
@@ -358,6 +358,9 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
|
||||
if (!bleOnlyMode) {
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
NavigationButton(to = "hearing_aid", stringResource(R.string.hearing_aid), navController)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
NoiseControlSettings(service = service)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -401,17 +404,8 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
service = service,
|
||||
sharedPreferences = sharedPreferences,
|
||||
default = false,
|
||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.off_listening_mode_description),
|
||||
style = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, top = 0.dp)
|
||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION,
|
||||
description = stringResource(R.string.off_listening_mode_description)
|
||||
)
|
||||
|
||||
// an about card- everything but the version number is unknown - will add later if i find out
|
||||
|
||||
@@ -0,0 +1,799 @@
|
||||
/*
|
||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
*
|
||||
* Copyright (C) 2025 LibrePods contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.screens
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
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.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.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
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import dev.chrisbanes.haze.HazeEffectScope
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.composables.AccessibilitySlider
|
||||
import me.kavishdevar.librepods.composables.IndependentToggle
|
||||
import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch
|
||||
import me.kavishdevar.librepods.composables.SinglePodANCSwitch
|
||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
||||
import me.kavishdevar.librepods.composables.ToneVolumeSlider
|
||||
import me.kavishdevar.librepods.composables.VolumeControlSwitch
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.utils.ATTManager
|
||||
import me.kavishdevar.librepods.utils.ATTHandles
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
private var debounceJob: Job? = null
|
||||
private var phoneMediaDebounceJob: Job? = null
|
||||
private const val TAG = "AccessibilitySettings"
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@ExperimentalHazeMaterialsApi
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
||||
@Composable
|
||||
fun HearingAidAdjustmentsScreen(navController: NavController) {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val verticalScrollState = rememberScrollState()
|
||||
val hazeState = remember { HazeState() }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val attManager = ServiceManager.getService()?.attManager ?: throw IllegalStateException("ATTManager not available")
|
||||
|
||||
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
||||
val context = LocalContext.current
|
||||
val radareOffsetFinder = remember { RadareOffsetFinder(context) }
|
||||
val isSdpOffsetAvailable = remember { mutableStateOf(RadareOffsetFinder.isSdpOffsetAvailable()) }
|
||||
val service = ServiceManager.getService()
|
||||
|
||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
||||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
||||
|
||||
Scaffold(
|
||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7),
|
||||
topBar = {
|
||||
val darkMode = isSystemInDarkTheme()
|
||||
val mDensity = remember { mutableFloatStateOf(1f) }
|
||||
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.adjustments),
|
||||
style = TextStyle(
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (darkMode) Color.White else Color.Black,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = CupertinoMaterials.thick(),
|
||||
block = fun HazeEffectScope.() {
|
||||
alpha = if (verticalScrollState.value > 60.dp.value * mDensity.floatValue) 1f else 0f
|
||||
})
|
||||
.drawBehind {
|
||||
mDensity.floatValue = density
|
||||
val strokeWidth = 0.7.dp.value * density
|
||||
val y = size.height - strokeWidth / 2
|
||||
if (verticalScrollState.value > 60.dp.value * density) {
|
||||
drawLine(
|
||||
if (darkMode) Color.DarkGray else Color.LightGray,
|
||||
Offset(0f, y),
|
||||
Offset(size.width, y),
|
||||
strokeWidth
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color.Transparent)
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.hazeSource(hazeState)
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(verticalScrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
|
||||
val enabled = remember { mutableStateOf(false) }
|
||||
val amplificationSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||
val balanceSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||
val toneSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||
val ambientNoiseReductionSliderValue = remember { mutableFloatStateOf(0.0f) }
|
||||
val conversationBoostEnabled = remember { mutableStateOf(false) }
|
||||
val eq = remember { mutableStateOf(FloatArray(8)) }
|
||||
|
||||
val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
|
||||
val phoneEQEnabled = remember { mutableStateOf(false) }
|
||||
val mediaEQEnabled = remember { mutableStateOf(false) }
|
||||
|
||||
val initialLoadComplete = remember { mutableStateOf(false) }
|
||||
|
||||
val initialReadSucceeded = remember { mutableStateOf(false) }
|
||||
val initialReadAttempts = remember { mutableStateOf(0) }
|
||||
|
||||
val HearingAidSettings = remember {
|
||||
mutableStateOf(
|
||||
HearingAidSettings(
|
||||
enabled = enabled.value,
|
||||
leftEQ = eq.value,
|
||||
rightEQ = eq.value,
|
||||
leftAmplification = amplificationSliderValue.floatValue + (0.5f - balanceSliderValue.floatValue) * amplificationSliderValue.floatValue * 2,
|
||||
rightAmplification = amplificationSliderValue.floatValue + (balanceSliderValue.floatValue - 0.5f) * amplificationSliderValue.floatValue * 2,
|
||||
leftTone = toneSliderValue.floatValue,
|
||||
rightTone = toneSliderValue.floatValue,
|
||||
leftConversationBoost = conversationBoostEnabled.value,
|
||||
rightConversationBoost = conversationBoostEnabled.value,
|
||||
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||
netAmplification = amplificationSliderValue.floatValue,
|
||||
balance = balanceSliderValue.floatValue
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value ||
|
||||
controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value) {
|
||||
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 }
|
||||
hearingAidEnabled.value = (aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(enabled.value, amplificationSliderValue.floatValue, balanceSliderValue.floatValue, toneSliderValue.floatValue, conversationBoostEnabled.value, ambientNoiseReductionSliderValue.floatValue, eq.value, initialLoadComplete.value, initialReadSucceeded.value) {
|
||||
if (!initialLoadComplete.value) {
|
||||
Log.d(TAG, "Initial device load not complete - skipping send")
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (!initialReadSucceeded.value) {
|
||||
Log.d(TAG, "Initial device read not successful yet - skipping send until read succeeds")
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
HearingAidSettings.value = HearingAidSettings(
|
||||
enabled = enabled.value,
|
||||
leftEQ = eq.value,
|
||||
rightEQ = eq.value,
|
||||
leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f,
|
||||
rightAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue > 0) balanceSliderValue.floatValue else 0f,
|
||||
leftTone = toneSliderValue.floatValue,
|
||||
rightTone = toneSliderValue.floatValue,
|
||||
leftConversationBoost = conversationBoostEnabled.value,
|
||||
rightConversationBoost = conversationBoostEnabled.value,
|
||||
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||
netAmplification = amplificationSliderValue.floatValue,
|
||||
balance = balanceSliderValue.floatValue
|
||||
)
|
||||
Log.d("HearingAidSettings", "Updated settings: ${HearingAidSettings.value}")
|
||||
// sendHearingAidSettings(attManager, HearingAidSettings.value)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
// attManager.unregisterListener(ATTHandles.TRANSPARENCY, transparencyListener)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
Log.d(TAG, "Connecting to ATT...")
|
||||
try {
|
||||
// attManager.enableNotifications(ATTHandles.TRANSPARENCY)
|
||||
// attManager.registerListener(ATTHandles.TRANSPARENCY, transparencyListener)
|
||||
|
||||
try {
|
||||
if (aacpManager != null) {
|
||||
Log.d(TAG, "Found AACPManager, reading cached EQ data")
|
||||
val aacpEQ = aacpManager.eqData
|
||||
if (aacpEQ.isNotEmpty()) {
|
||||
eq.value = aacpEQ.copyOf()
|
||||
phoneMediaEQ.value = aacpEQ.copyOf()
|
||||
phoneEQEnabled.value = aacpManager.eqOnPhone
|
||||
mediaEQEnabled.value = aacpManager.eqOnMedia
|
||||
Log.d(TAG, "Populated EQ from AACPManager: ${aacpEQ.toList()}")
|
||||
} else {
|
||||
Log.d(TAG, "AACPManager EQ data empty")
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "No AACPManager available")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
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)
|
||||
parsedSettings = parseHearingAidSettingsResponse(data = data)
|
||||
if (parsedSettings != null) {
|
||||
Log.d(TAG, "Parsed settings on attempt $attempt")
|
||||
break
|
||||
} else {
|
||||
Log.d(TAG, "Parsing returned null on attempt $attempt")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Read attempt $attempt failed: ${e.message}")
|
||||
}
|
||||
delay(200)
|
||||
}
|
||||
|
||||
if (parsedSettings != null) {
|
||||
Log.d(TAG, "Initial transparency settings: $parsedSettings")
|
||||
enabled.value = parsedSettings.enabled
|
||||
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()
|
||||
initialReadSucceeded.value = true
|
||||
} else {
|
||||
Log.d(TAG, "Failed to read/parse initial transparency settings after ${initialReadAttempts.value} attempts")
|
||||
}
|
||||
*/
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
initialLoadComplete.value = true
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.amplification).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
||||
.height(55.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Text(
|
||||
text = "",
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(start = 4.dp)
|
||||
)
|
||||
AccessibilitySlider(
|
||||
valueRange = -1f..1f,
|
||||
value = amplificationSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
amplificationSliderValue.floatValue = snapIfClose(it, listOf(-0.5f, -0.25f, 0f, 0.25f, 0.5f))
|
||||
},
|
||||
widthFrac = 0.90f
|
||||
)
|
||||
Text(
|
||||
text = "",
|
||||
style = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(end = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
|
||||
IndependentToggle(
|
||||
name = stringResource(R.string.swipe_to_control_amplification),
|
||||
service = service,
|
||||
sharedPreferences = sharedPreferences,
|
||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.HPS_GAIN_SWIPE,
|
||||
description = stringResource(R.string.swipe_amplification_description)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.balance).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.left),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.right),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
}
|
||||
AccessibilitySlider(
|
||||
valueRange = -1f..1f,
|
||||
value = balanceSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
balanceSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.tone).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.darker),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.brighter),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
}
|
||||
AccessibilitySlider(
|
||||
valueRange = -1f..1f,
|
||||
value = toneSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
toneSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.ambient_noise_reduction).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.less),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.more),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
}
|
||||
AccessibilitySlider(
|
||||
valueRange = 0f..1f,
|
||||
value = ambientNoiseReductionSliderValue.floatValue,
|
||||
onValueChange = {
|
||||
ambientNoiseReductionSliderValue.floatValue = snapIfClose(it, listOf(0.1f, 0.3f, 0.5f, 0.7f, 0.9f))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AccessibilityToggle(
|
||||
text = stringResource(R.string.conversation_boost),
|
||||
mutableState = conversationBoostEnabled,
|
||||
independent = true,
|
||||
description = stringResource(R.string.conversation_boost_description)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class HearingAidSettings(
|
||||
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 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
|
||||
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 parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings? {
|
||||
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 HearingAidSettings(
|
||||
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 sendHearingAidSettings(
|
||||
attManager: ATTManager,
|
||||
HearingAidSettings: HearingAidSettings
|
||||
) {
|
||||
debounceJob?.cancel()
|
||||
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")
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
*
|
||||
* Copyright (C) 2025 LibrePods contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.screens
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
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.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.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
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import dev.chrisbanes.haze.HazeEffectScope
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.composables.AccessibilitySlider
|
||||
import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch
|
||||
import me.kavishdevar.librepods.composables.SinglePodANCSwitch
|
||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
||||
import me.kavishdevar.librepods.composables.ToneVolumeSlider
|
||||
import me.kavishdevar.librepods.composables.VolumeControlSwitch
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.utils.ATTManager
|
||||
import me.kavishdevar.librepods.utils.ATTHandles
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
private var debounceJob: Job? = null
|
||||
private var phoneMediaDebounceJob: Job? = null
|
||||
private const val TAG = "AccessibilitySettings"
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@ExperimentalHazeMaterialsApi
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
||||
@Composable
|
||||
fun HearingAidScreen(navController: NavController) {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val verticalScrollState = rememberScrollState()
|
||||
val hazeState = remember { HazeState() }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val attManager = ServiceManager.getService()?.attManager ?: throw IllegalStateException("ATTManager not available")
|
||||
|
||||
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
||||
val context = LocalContext.current
|
||||
val radareOffsetFinder = remember { RadareOffsetFinder(context) }
|
||||
val isSdpOffsetAvailable = remember { mutableStateOf(RadareOffsetFinder.isSdpOffsetAvailable()) }
|
||||
val service = ServiceManager.getService()
|
||||
|
||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
||||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
||||
|
||||
Scaffold(
|
||||
containerColor = if (isSystemInDarkTheme()) Color(
|
||||
0xFF000000
|
||||
) else Color(
|
||||
0xFFF2F2F7
|
||||
),
|
||||
topBar = {
|
||||
val darkMode = isSystemInDarkTheme()
|
||||
val mDensity = remember { mutableFloatStateOf(1f) }
|
||||
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.hearing_aid),
|
||||
style = TextStyle(
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (darkMode) Color.White else Color.Black,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = CupertinoMaterials.thick(),
|
||||
block = fun HazeEffectScope.() {
|
||||
alpha =
|
||||
if (verticalScrollState.value > 60.dp.value * mDensity.floatValue) 1f else 0f
|
||||
})
|
||||
.drawBehind {
|
||||
mDensity.floatValue = density
|
||||
val strokeWidth = 0.7.dp.value * density
|
||||
val y = size.height - strokeWidth / 2
|
||||
if (verticalScrollState.value > 60.dp.value * density) {
|
||||
drawLine(
|
||||
if (darkMode) Color.DarkGray else Color.LightGray,
|
||||
Offset(0f, y),
|
||||
Offset(size.width, y),
|
||||
strokeWidth
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.hazeSource(hazeState)
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(horizontal = 16.dp)
|
||||
.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) {
|
||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value ||
|
||||
controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value) {
|
||||
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 }
|
||||
hearingAidEnabled.value = (aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
} 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
|
||||
}
|
||||
hearingAidEnabled.value = value
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.hearing_aid).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f)
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||
)
|
||||
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
) {
|
||||
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))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
backgroundColorHA = if (isDarkThemeLocal) Color(0x40888888) else Color(0x40D9D9D9)
|
||||
tryAwaitRelease()
|
||||
backgroundColorHA = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
},
|
||||
onTap = {
|
||||
onChange(value = !hearingAidEnabled.value)
|
||||
}
|
||||
)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = stringResource(R.string.hearing_aid), modifier = Modifier.weight(1f), fontSize = 16.sp, color = textColor)
|
||||
StyledSwitch(
|
||||
checked = hearingAidEnabled.value,
|
||||
onCheckedChange = {
|
||||
onChange(value = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { navController.navigate("hearing_aid_adjustments") }
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.adjustments),
|
||||
fontSize = 16.sp,
|
||||
color = textColor
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
contentDescription = null,
|
||||
tint = textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.hearing_aid_description),
|
||||
style = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,4 +100,34 @@
|
||||
<string name="hang_up">Hang Up</string>
|
||||
<string name="press_once">Press Once</string>
|
||||
<string name="press_twice">Press Twice</string>
|
||||
<string name="hearing_aid">Hearing Aid</string>
|
||||
<string name="adjustments">Adjustments</string>
|
||||
<string name="swipe_to_control_amplification">Swipe to control amplification</string>
|
||||
<string name="swipe_amplification_description">When in Transparency and no media is playing, swipe up and down on the Touch controls of your AirPods Pro to increase or decrease the amplification of environmental sounds.</string>
|
||||
<string name="transparency_mode">Transparency Mode</string>
|
||||
<string name="customize_transparency_mode">Customize Transparency Mode</string>
|
||||
<string name="press_speed">Press Speed</string>
|
||||
<string name="press_and_hold_duration">Press and Hold Duration</string>
|
||||
<string name="volume_swipe_speed">Volume Swipe Speed</string>
|
||||
<string name="equalizer">Equalizer</string>
|
||||
<string name="apply_eq_to">Apply EQ to</string>
|
||||
<string name="phone">Phone</string>
|
||||
<string name="media">Media</string>
|
||||
<string name="band_label">Band %d</string>
|
||||
<string name="default_option">Default</string>
|
||||
<string name="slower">Slower</string>
|
||||
<string name="slowest">Slowest</string>
|
||||
<string name="longer">Longer</string>
|
||||
<string name="longest">Longest</string>
|
||||
<string name="darker">Darker</string>
|
||||
<string name="brighter">Brighter</string>
|
||||
<string name="less">Less</string>
|
||||
<string name="more">More</string>
|
||||
<string name="amplification">Amplification</string>
|
||||
<string name="balance">Balance</string>
|
||||
<string name="tone">Tone</string>
|
||||
<string name="ambient_noise_reduction">Ambient Noise Reduction</string>
|
||||
<string name="conversation_boost">Conversation Boost</string>
|
||||
<string name="conversation_boost_description">Conversation Boost focuses your AirPods Pro on the person talking in front of you, making it easier to hear in a face-to-face conversation.</string>
|
||||
<string name="hearing_aid_description">AirPods can use the results of a hearing test to make adjustments that improve the clarity of voices and sounds around you. \n\n Hearing Aid is only intended for people with perceived mild to moderate hearing loss.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user