diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt index f3a31abb..d57f2cd9 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt @@ -42,6 +42,7 @@ data class RadioCompressorSettings(val enabled: Boolean = true, val maxGainDb: I * При формировании конфига настройки и keys.conf читаются один раз; чтение/запись utun.cfg сериализованы. */ class ConfigProvider(private val context: Context) { + val changes = SettingsOperations() private val configFiles = ConfigFileStore { level, message -> LogManager.addLog(level, "CFG", message) } private val bleFlags = intPreferencesKey("bluetooth_flags") fun getBleFlags(): Int = runBlocking { context.dataStore.data.first()[bleFlags] ?: 0 } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/SettingsOperations.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/SettingsOperations.kt new file mode 100644 index 00000000..eb656eec --- /dev/null +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/SettingsOperations.kt @@ -0,0 +1,40 @@ +package com.utun.chat.data + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +/** Очередь Application на main: сохранить выбор, затем применить runtime без зависимости от UI scope. + * submit принимает неизменный снимок аргументов caller; отмена await не отменяет запись. + * awaitIdle ставит барьер после принятых изменений перед формированием restart-конфига. */ +class SettingsOperations(private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate), + private val log: (String, String) -> Unit = { level, message -> LogManager.addLog(level, "Settings", message) }) { + private val mutex = Mutex() + private val _failed = MutableStateFlow(false) + val failed = _failed.asStateFlow() + + fun submit(name: String, operation: suspend () -> Unit): Deferred = scope.async(start = CoroutineStart.UNDISPATCHED) { + mutex.withLock { + try { + operation() + log("INFO", "Setting applied name=$name") + } catch (e: CancellationException) { throw e } + catch (e: Exception) { reportFailure(name, e); throw e } + } + } + + fun reportFailure(name: String, error: Exception) { + _failed.value = true + log("ERROR", "Setting failed name=$name: ${error.javaClass.simpleName}: ${error.message}") + } + fun dismissFailure() { _failed.value = false } + suspend fun awaitIdle() { mutex.withLock { } } +} diff --git a/tools/chatgui-android/router-tests/build.gradle.kts b/tools/chatgui-android/router-tests/build.gradle.kts index 6ffda6ce..7cd2f070 100644 --- a/tools/chatgui-android/router-tests/build.gradle.kts +++ b/tools/chatgui-android/router-tests/build.gradle.kts @@ -28,6 +28,7 @@ kotlin { "com/utun/chat/data/CallPlaybackBuffer.kt", "com/utun/chat/data/CallOutputRecovery.kt", "com/utun/chat/data/CallState.kt", "com/utun/chat/data/CallAudioHost.kt") kotlin.include("com/utun/chat/data/ConversationSubmissions.kt", "com/utun/chat/data/ConversationTarget.kt") + kotlin.include("com/utun/chat/data/SettingsOperations.kt") } }