Load confirmed settings after pending changes and guard invalid audio preferences

This commit is contained in:
evgeny committed 2026-10-29 13:35:41 +01:00
1 parent 48564e8ab3
commit c5c4d0e588
2 files changed
+39 -18

No files matched your search

@@ -475,7 +475,7 @@ class ConfigProvider(private val context: Context) {
fun getVoiceSettings(): VoiceSettings = runBlocking {
val prefs = context.dataStore.data.first()
val defaults = VoiceSettings()
val settings = VoiceSettings(
val raw = VoiceSettings(
preset = prefs[chatOpusCodecPreset] ?: defaults.preset,
compressorEnabled = (prefs[chatCompressorEnabled] ?: 1) != 0,
maxGainDb = prefs[chatCompressorMaxGainDb] ?: defaults.maxGainDb,
@@ -484,6 +484,10 @@ class ConfigProvider(private val context: Context) {
riseRate = prefs[chatCompressorRiseRate] ?: defaults.riseRate,
targetLevelDb = prefs[chatCompressorTargetLevelDb] ?: defaults.targetLevelDb
)
val settings = raw.copy(preset = raw.preset.coerceIn(0, 2), maxGainDb = raw.maxGainDb.coerceIn(0, 30),
lookbackMs = raw.lookbackMs.coerceIn(50, 500), lookaheadMs = raw.lookaheadMs.coerceIn(20, 500),
riseRate = raw.riseRate.coerceIn(2, 10), targetLevelDb = raw.targetLevelDb.coerceIn(-40, 0))
if (raw != settings) LogManager.addLog("WARN", "AudioSettings", "Invalid saved voice settings normalized: $raw -> $settings")
LogManager.addLog("INFO", "AudioSettings", "Loaded: $settings")
settings
}
@@ -76,6 +76,12 @@ import java.io.IOException
private val LEVELS = listOf("DISABLED", "NONE", "ERROR", "WARN", "INFO", "DEBUG", "TRACE")
private val PROTOCOLS = listOf("tcp", "udp")
private fun parseDebugPreferences(text: String): Map<String, String> = text.split(',').mapNotNull { entry ->
val pair = entry.trim().split('=', limit = 2)
if (pair.size == 2 && pair[0].isNotBlank() && pair[1].uppercase() in LEVELS) pair[0].trim() to pair[1].uppercase()
else { if (entry.isNotBlank()) LogManager.addLog("WARN", "CFG", "Malformed debug category preference ignored"); null }
}.toMap()
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = {}) {
@@ -88,7 +94,7 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = {
var loadAttempt by remember { mutableIntStateOf(0) }
LaunchedEffect(loadAttempt) {
loadFailed = false
try { withContext(Dispatchers.IO) { provider.allValues.first() }; loaded = true }
try { changes.awaitIdle(); withContext(Dispatchers.IO) { provider.allValues.first() }; loaded = true }
catch (e: CancellationException) { throw e }
catch (e: Exception) { loadFailed = true; LogManager.addLog("ERROR", "Settings", "Settings load failed: $e") }
}
@@ -163,14 +169,18 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = {
Tab(selected = tabIndex == idx, onClick = { tabIndex = idx }, text = { Text(stringResource(name), maxLines = 1) })
}
}
when (tabIndex) {
key(tabIndex) {
var tabReady by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { changes.awaitIdle(); tabReady = true }
if (tabReady) when (tabIndex) {
0 -> GeneralSettingsTab(onDirty = { dirty = it })
1 -> AudioSettingsTab(vm)
2 -> StorageSettingsTab()
3 -> RadioSettingsTab()
4 -> StreamSettingsTab(vm)
4 -> StreamSettingsTab()
5 -> DeveloperSettingsTab(vm, onDirty = { dirty = it }, onExit)
6 -> BluetoothSettingsTab()
} else CircularProgressIndicator(Modifier.padding(16.dp))
}
}
}
@@ -178,7 +188,7 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = {
/* Параметры RS сохраняются для следующей эпохи трансляции. */
@Composable
private fun StreamSettingsTab(vm: ChatViewModel) {
private fun StreamSettingsTab() {
val provider = ChatApplication.instance.configProvider
var subblocks by rememberSaveable { mutableStateOf(provider.getInt("chat.stream_subblocks").toString()) }
var redundant by rememberSaveable { mutableStateOf(provider.getInt("chat.stream_rs_redundant").toString()) }
@@ -240,8 +250,11 @@ private fun GeneralSettingsTab(onDirty: (Boolean) -> Unit) {
var localBroadcastsEnabled by remember { mutableStateOf(provider.getInt("chat.local_broadcasts_enabled") != 0) }
LaunchedEffect(Unit) {
nodeName = provider.getString("node.name") ?: provider.getDeviceName()
LogManager.addLog("INFO", "SETTINGS", "General settings opened")
try {
nodeName = provider.getString("node.name") ?: provider.getDeviceName()
LogManager.addLog("INFO", "SETTINGS", "General settings opened")
} catch (e: CancellationException) { throw e }
catch (e: Exception) { provider.changes.reportFailure("load general settings", e) }
}
LazyColumn(
@@ -369,11 +382,7 @@ private fun DeveloperSettingsTab(vm: ChatViewModel, onDirty: (Boolean) -> Unit,
standbyMinSleepSec = provider.getInt("chat.standby_min_sleep_sec").toString()
standbyKeepaliveMs = provider.getInt("chat.standby_keepalive_ms").toString()
val stored = provider.getString("debug.categories") ?: ""
catLevels = stored.split(',').mapNotNull { entry ->
val pair = entry.trim().split('=', limit = 2)
if (pair.size == 2 && pair[0].isNotBlank() && pair[1].uppercase() in LEVELS) pair[0].trim() to pair[1].uppercase()
else { if (entry.isNotBlank()) LogManager.addLog("WARN", "CFG", "Malformed debug category preference ignored"); null }
}.toMap()
catLevels = parseDebugPreferences(stored)
try { servers = provider.loadServersFromConfig().await()
serversLoaded = true
} catch (e: CancellationException) { throw e
@@ -425,10 +434,10 @@ private fun DeveloperSettingsTab(vm: ChatViewModel, onDirty: (Boolean) -> Unit,
}
}
val setCatLevel: (String, String) -> Unit = { cat, lvl ->
val updated = catLevels.toMutableMap()
if (lvl == "") updated.remove(cat) else updated[cat] = lvl
val csv = updated.entries.joinToString(",") { "${it.key}=${it.value}" }
provider.changes.submit("debug category") {
val updated = parseDebugPreferences(provider.getString("debug.categories").orEmpty()).toMutableMap()
if (lvl.isEmpty()) updated.remove(cat) else updated[cat] = lvl
val csv = updated.entries.joinToString(",") { "${it.key}=${it.value}" }
provider.setValue("debug.categories", csv); catLevels = updated
NativeLib.setDebugLevel(cat, lvl.ifEmpty { "NONE" })
}
@@ -815,10 +824,14 @@ private fun DeveloperSettingsTab(vm: ChatViewModel, onDirty: (Boolean) -> Unit,
item(key = "exit-spacer") { Spacer(Modifier.height(8.dp)) }
item(key = "exit") {
Button(onClick = {
provider.changes.submit("stop service and exit") {
val request = provider.changes.submit("stop service and exit") {
provider.setServiceEnabled(false)
ChatApplication.instance.stopService(Intent(ChatApplication.instance, HeadlessService::class.java))
onExit()
}
scope.launch {
try { request.await(); onExit() }
catch (e: CancellationException) { throw e }
catch (e: Exception) { provider.changes.reportFailure("exit after service stop", e) }
}
}, modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)) {
@@ -881,7 +894,7 @@ internal fun RadioAutomaticSettings(provider: ConfigProvider, compact: Boolean =
}
LaunchedEffect(resumed) {
if (!resumed) { level = null; vadProbability = null; vadGain = null; return@LaunchedEffect }
while (true) {
try { while (true) {
val status = NativeLib.radioInputStatus()
active = engine.isActive()
micPermission = MicrophonePermission.granted()
@@ -891,6 +904,10 @@ internal fun RadioAutomaticSettings(provider: ConfigProvider, compact: Boolean =
vadCompressorEnabled = status[6] != 0f
blocked = status[2] != 0f
delay(100)
} } catch (e: CancellationException) { throw e }
catch (e: Exception) {
level = null; vadProbability = null; vadGain = null
provider.changes.reportFailure("radio meter", e)
}
}
if (!compact) Text(stringResource(R.string.radio_automatic), style = MaterialTheme.typography.titleMedium)