mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-10 19:52:24 +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.AppSettingsScreen
|
||||||
import me.kavishdevar.librepods.screens.DebugScreen
|
import me.kavishdevar.librepods.screens.DebugScreen
|
||||||
import me.kavishdevar.librepods.screens.HeadTrackingScreen
|
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.LongPress
|
||||||
import me.kavishdevar.librepods.screens.Onboarding
|
import me.kavishdevar.librepods.screens.Onboarding
|
||||||
import me.kavishdevar.librepods.screens.RenameScreen
|
import me.kavishdevar.librepods.screens.RenameScreen
|
||||||
@@ -380,6 +382,12 @@ fun Main() {
|
|||||||
composable("onboarding") {
|
composable("onboarding") {
|
||||||
Onboarding(navController, context)
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AccessibilitySlider(
|
fun AccessibilitySlider(
|
||||||
label: String,
|
label: String? = null,
|
||||||
value: Float,
|
value: Float,
|
||||||
onValueChange: (Float) -> Unit,
|
onValueChange: (Float) -> Unit,
|
||||||
valueRange: ClosedFloatingPointRange<Float>
|
valueRange: ClosedFloatingPointRange<Float>,
|
||||||
|
widthFrac: Float = 1f
|
||||||
) {
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
@@ -62,18 +63,20 @@ fun AccessibilitySlider(
|
|||||||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(widthFrac),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
if (label != null) {
|
||||||
text = label,
|
Text(
|
||||||
style = TextStyle(
|
text = label,
|
||||||
fontSize = 16.sp,
|
style = TextStyle(
|
||||||
fontWeight = FontWeight.Medium,
|
fontSize = 16.sp,
|
||||||
color = labelTextColor,
|
fontWeight = FontWeight.Medium,
|
||||||
fontFamily = androidx.compose.ui.text.font.FontFamily(Font(R.font.sf_pro))
|
color = labelTextColor,
|
||||||
|
fontFamily = androidx.compose.ui.text.font.FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
Slider(
|
Slider(
|
||||||
value = value,
|
value = value,
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
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.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import me.kavishdevar.librepods.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
@Composable
|
@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 isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val snakeCasedName =
|
val snakeCasedName =
|
||||||
@@ -109,39 +117,57 @@ fun IndependentToggle(name: String, service: AirPodsService? = null, functionNam
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box (
|
Column (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp),
|
||||||
.background(animatedBackgroundColor, RoundedCornerShape(14.dp))
|
) {
|
||||||
.pointerInput(Unit) {
|
Box (
|
||||||
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
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.background(animatedBackgroundColor, RoundedCornerShape(14.dp))
|
||||||
.height(55.dp)
|
.pointerInput(Unit) {
|
||||||
.padding(horizontal = 12.dp),
|
detectTapGestures(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
onPress = {
|
||||||
) {
|
backgroundColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
Text(text = name, modifier = Modifier.weight(1f), fontSize = 16.sp, color = textColor)
|
tryAwaitRelease()
|
||||||
StyledSwitch(
|
backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
checked = checked,
|
},
|
||||||
onCheckedChange = {
|
onTap = {
|
||||||
checked = it
|
checked = !checked
|
||||||
cb()
|
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 java.nio.ByteOrder
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
var debounceJob: Job? = null
|
private var debounceJob: Job? = null
|
||||||
var phoneMediaDebounceJob: Job? = null
|
private var phoneMediaDebounceJob: Job? = null
|
||||||
const val TAG = "AccessibilitySettings"
|
private const val TAG = "AccessibilitySettings"
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@@ -150,7 +150,7 @@ fun AccessibilitySettingsScreen() {
|
|||||||
CenterAlignedTopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = "Accessibility Settings",
|
text = stringResource(R.string.accessibility),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
@@ -452,79 +452,232 @@ fun AccessibilitySettingsScreen() {
|
|||||||
// Only show transparency mode section if SDP offset is available
|
// Only show transparency mode section if SDP offset is available
|
||||||
if (isSdpOffsetAvailable.value) {
|
if (isSdpOffsetAvailable.value) {
|
||||||
AccessibilityToggle(
|
AccessibilityToggle(
|
||||||
text = "Transparency Mode",
|
text = stringResource(R.string.transparency_mode),
|
||||||
mutableState = enabled,
|
mutableState = enabled,
|
||||||
independent = true
|
independent = true,
|
||||||
)
|
description = stringResource(R.string.customize_transparency_mode_description)
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Customize Transparency Mode".uppercase(),
|
text = stringResource(R.string.amplification).uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
color = textColor.copy(alpha = 0.6f),
|
color = textColor.copy(alpha = 0.6f),
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
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
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
.padding(8.dp)
|
.padding(horizontal = 8.dp, vertical = 0.dp)
|
||||||
|
.height(55.dp)
|
||||||
) {
|
) {
|
||||||
AccessibilitySlider(
|
Row(
|
||||||
label = "Amplification",
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
valueRange = -1f..1f,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
value = amplificationSliderValue.floatValue,
|
modifier = Modifier.fillMaxSize()
|
||||||
onValueChange = {
|
) {
|
||||||
amplificationSliderValue.floatValue = snapIfClose(it, listOf(-0.5f, -0.25f, 0f, 0.25f, 0.5f))
|
Text(
|
||||||
},
|
text = "",
|
||||||
)
|
style = TextStyle(
|
||||||
AccessibilitySlider(
|
fontSize = 16.sp,
|
||||||
label = "Balance",
|
fontWeight = FontWeight.Normal,
|
||||||
valueRange = -1f..1f,
|
color = labelTextColor,
|
||||||
value = balanceSliderValue.floatValue,
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
onValueChange = {
|
),
|
||||||
balanceSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
modifier = Modifier.padding(start = 4.dp)
|
||||||
},
|
)
|
||||||
)
|
AccessibilitySlider(
|
||||||
AccessibilitySlider(
|
valueRange = -1f..1f,
|
||||||
label = "Tone",
|
value = amplificationSliderValue.floatValue,
|
||||||
valueRange = -1f..1f,
|
onValueChange = {
|
||||||
value = toneSliderValue.floatValue,
|
amplificationSliderValue.floatValue = snapIfClose(it, listOf(-0.5f, -0.25f, 0f, 0.25f, 0.5f))
|
||||||
onValueChange = {
|
},
|
||||||
toneSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
widthFrac = 0.90f,
|
||||||
},
|
)
|
||||||
)
|
Text(
|
||||||
AccessibilitySlider(
|
text = "",
|
||||||
label = "Ambient Noise Reduction",
|
style = TextStyle(
|
||||||
valueRange = 0f..1f,
|
fontSize = 16.sp,
|
||||||
value = ambientNoiseReductionSliderValue.floatValue,
|
fontWeight = FontWeight.Normal,
|
||||||
onValueChange = {
|
color = labelTextColor,
|
||||||
ambientNoiseReductionSliderValue.floatValue = snapIfClose(it, listOf(0.1f, 0.3f, 0.5f, 0.7f, 0.9f))
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
},
|
),
|
||||||
)
|
modifier = Modifier.padding(end = 4.dp)
|
||||||
AccessibilityToggle(
|
)
|
||||||
text = "Conversation Boost",
|
}
|
||||||
mutableState = conversationBoostEnabled
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
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(
|
||||||
text = "AUDIO",
|
text = stringResource(R.string.audio).uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
@@ -542,7 +695,7 @@ fun AccessibilitySettingsScreen() {
|
|||||||
verticalArrangement = Arrangement.SpaceBetween
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Tone Volume",
|
text = stringResource(R.string.tone_volume),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
@@ -559,8 +712,8 @@ fun AccessibilitySettingsScreen() {
|
|||||||
LoudSoundReductionSwitch()
|
LoudSoundReductionSwitch()
|
||||||
|
|
||||||
DropdownMenuComponent(
|
DropdownMenuComponent(
|
||||||
label = "Press Speed",
|
label = stringResource(R.string.press_speed),
|
||||||
options = pressSpeedOptions.values.toList(),
|
options = listOf(stringResource(R.string.default_option), stringResource(R.string.slower), stringResource(R.string.slowest)),
|
||||||
selectedOption = selectedPressSpeed.toString(),
|
selectedOption = selectedPressSpeed.toString(),
|
||||||
onOptionSelected = { newValue ->
|
onOptionSelected = { newValue ->
|
||||||
selectedPressSpeed = newValue
|
selectedPressSpeed = newValue
|
||||||
@@ -572,8 +725,8 @@ fun AccessibilitySettingsScreen() {
|
|||||||
textColor = textColor
|
textColor = textColor
|
||||||
)
|
)
|
||||||
DropdownMenuComponent(
|
DropdownMenuComponent(
|
||||||
label = "Press and Hold Duration",
|
label = stringResource(R.string.press_and_hold_duration),
|
||||||
options = pressAndHoldDurationOptions.values.toList(),
|
options = listOf(stringResource(R.string.default_option), stringResource(R.string.slower), stringResource(R.string.slowest)),
|
||||||
selectedOption = selectedPressAndHoldDuration.toString(),
|
selectedOption = selectedPressAndHoldDuration.toString(),
|
||||||
onOptionSelected = { newValue ->
|
onOptionSelected = { newValue ->
|
||||||
selectedPressAndHoldDuration = newValue
|
selectedPressAndHoldDuration = newValue
|
||||||
@@ -585,8 +738,8 @@ fun AccessibilitySettingsScreen() {
|
|||||||
textColor = textColor
|
textColor = textColor
|
||||||
)
|
)
|
||||||
DropdownMenuComponent(
|
DropdownMenuComponent(
|
||||||
label = "Volume Swipe Speed",
|
label = stringResource(R.string.volume_swipe_speed),
|
||||||
options = volumeSwipeSpeedOptions.values.toList(),
|
options = listOf(stringResource(R.string.default_option), stringResource(R.string.longer), stringResource(R.string.longest)),
|
||||||
selectedOption = selectedVolumeSwipeSpeed.toString(),
|
selectedOption = selectedVolumeSwipeSpeed.toString(),
|
||||||
onOptionSelected = { newValue ->
|
onOptionSelected = { newValue ->
|
||||||
selectedVolumeSwipeSpeed = newValue
|
selectedVolumeSwipeSpeed = newValue
|
||||||
@@ -603,7 +756,7 @@ fun AccessibilitySettingsScreen() {
|
|||||||
// Only show transparency mode EQ section if SDP offset is available
|
// Only show transparency mode EQ section if SDP offset is available
|
||||||
if (isSdpOffsetAvailable.value) {
|
if (isSdpOffsetAvailable.value) {
|
||||||
Text(
|
Text(
|
||||||
text = "Equalizer".uppercase(),
|
text = stringResource(R.string.equalizer).uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
@@ -687,7 +840,7 @@ fun AccessibilitySettingsScreen() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Band ${i + 1}",
|
text = stringResource(R.string.band_label, i + 1),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
modifier = Modifier.padding(top = 4.dp)
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
@@ -700,7 +853,7 @@ fun AccessibilitySettingsScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Apply EQ to".uppercase(),
|
text = stringResource(R.string.apply_eq_to).uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
@@ -740,7 +893,7 @@ fun AccessibilitySettingsScreen() {
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Phone",
|
stringResource(R.string.phone),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
@@ -791,7 +944,7 @@ fun AccessibilitySettingsScreen() {
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Media",
|
stringResource(R.string.media),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
@@ -888,7 +1041,7 @@ fun AccessibilitySettingsScreen() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Band ${i + 1}",
|
text = stringResource(R.string.band_label, i + 1),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
modifier = Modifier.padding(top = 4.dp)
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
@@ -902,57 +1055,75 @@ fun AccessibilitySettingsScreen() {
|
|||||||
|
|
||||||
|
|
||||||
@Composable
|
@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()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
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)
|
val cornerShape = if (independent) RoundedCornerShape(14.dp) else RoundedCornerShape(0.dp)
|
||||||
Box (
|
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = boxPaddings)
|
.padding(vertical = 8.dp)
|
||||||
.background(animatedBackgroundColor, cornerShape)
|
) {
|
||||||
.pointerInput(Unit) {
|
Box (
|
||||||
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
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.background(animatedBackgroundColor, cornerShape)
|
||||||
.height(rowHeight)
|
.pointerInput(Unit) {
|
||||||
.padding(horizontal = rowPadding),
|
detectTapGestures(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
onPress = {
|
||||||
) {
|
backgroundColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
Text(
|
tryAwaitRelease()
|
||||||
text = text,
|
backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
modifier = Modifier.weight(1f),
|
},
|
||||||
fontSize = 16.sp,
|
onTap = {
|
||||||
color = textColor
|
mutableState.value = !mutableState.value
|
||||||
)
|
}
|
||||||
StyledSwitch(
|
)
|
||||||
checked = mutableState.value,
|
|
||||||
onCheckedChange = {
|
|
||||||
mutableState.value = it
|
|
||||||
},
|
},
|
||||||
|
)
|
||||||
|
{
|
||||||
|
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 enabled: Boolean,
|
||||||
val leftEQ: FloatArray,
|
val leftEQ: FloatArray,
|
||||||
val rightEQ: FloatArray,
|
val rightEQ: FloatArray,
|
||||||
@@ -1134,7 +1305,7 @@ private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DropdownMenuComponent(
|
private fun DropdownMenuComponent(
|
||||||
label: String,
|
label: String,
|
||||||
options: List<String>,
|
options: List<String>,
|
||||||
selectedOption: String,
|
selectedOption: String,
|
||||||
|
|||||||
@@ -358,6 +358,9 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
|
|
||||||
if (!bleOnlyMode) {
|
if (!bleOnlyMode) {
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
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)
|
NoiseControlSettings(service = service)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -401,17 +404,8 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
service = service,
|
service = service,
|
||||||
sharedPreferences = sharedPreferences,
|
sharedPreferences = sharedPreferences,
|
||||||
default = false,
|
default = false,
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
|
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION,
|
||||||
)
|
description = stringResource(R.string.off_listening_mode_description)
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// an about card- everything but the version number is unknown - will add later if i find out
|
// 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="hang_up">Hang Up</string>
|
||||||
<string name="press_once">Press Once</string>
|
<string name="press_once">Press Once</string>
|
||||||
<string name="press_twice">Press Twice</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>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user