From 0a36dc5ac6386a0d0fe2cbd22cdfb6c8db57d664 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 28 Oct 2026 11:34:42 +0100 Subject: [PATCH] Make Android UDP log forwarding opt-in with a persisted target --- .../java/com/utun/chat/data/ConfigProvider.kt | 39 ++++++++++++------ .../java/com/utun/chat/data/UdpLogSettings.kt | 24 +++++++++++ .../com/utun/chat/data/UdpLogSettingsTest.kt | 40 +++++++++++++++++++ 3 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 tools/chatgui-android/app/src/main/java/com/utun/chat/data/UdpLogSettings.kt create mode 100644 tools/chatgui-android/app/src/test/java/com/utun/chat/data/UdpLogSettingsTest.kt 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 6d81748a..dc62c734 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 @@ -55,6 +55,7 @@ class ConfigProvider(private val context: Context) { private val debugCategories = stringPreferencesKey("debug_categories") private val logUdpIp = stringPreferencesKey("log_udp_ip") private val logUdpPort = intPreferencesKey("log_udp_port") + private val logUdpEnabled = booleanPreferencesKey("log_udp_enabled") private val chatStorageAutoload = intPreferencesKey("chat_storage_autoload") private val chatStorageAutoloadMaxsizeMb = intPreferencesKey("chat_storage_autoload_maxsize_mb") private val chatStorageMaxsizeGb = intPreferencesKey("chat_storage_maxsize_gb") @@ -184,11 +185,27 @@ class ConfigProvider(private val context: Context) { "debug.console_level" to (prefs[debugConsoleLevel] ?: "info"), "debug.log_file" to (prefs[debugLogFile] ?: "utun.log"), "debug.categories" to (prefs[debugCategories] ?: ""), - "log_udp.ip" to (prefs[logUdpIp] ?: ""), - "log_udp.port" to (prefs[logUdpPort]?.toString() ?: "0") + "log_udp.enabled" to if (prefs[logUdpEnabled] ?: false) "1" else "0", + "log_udp.ip" to (prefs[logUdpIp] ?: UdpLogSettings.DEFAULT_IP), + "log_udp.port" to (prefs[logUdpPort]?.toString() ?: UdpLogSettings.DEFAULT_PORT.toString()) ) } + private fun udpLogSettings(prefs: Preferences): UdpLogSettings = UdpLogSettings(prefs[logUdpEnabled] ?: false, + prefs[logUdpIp] ?: UdpLogSettings.DEFAULT_IP, prefs[logUdpPort] ?: UdpLogSettings.DEFAULT_PORT) + + suspend fun getUdpLogSettings(): UdpLogSettings = configFiles.submit("load UDP log settings") { + udpLogSettings(context.dataStore.data.first()) + }.await() + + fun saveUdpLogSettings(settings: UdpLogSettings): Deferred = configFiles.submit("save UDP log settings") { + context.dataStore.edit { prefs -> + prefs[logUdpEnabled] = settings.enabled; prefs[logUdpIp] = settings.ip; prefs[logUdpPort] = settings.port + } + LogManager.addLog("INFO", "UDPLog", "Settings saved enabled=${settings.enabled} active=${settings.active} " + + "target=${settings.ip}:${settings.port}") + } + fun getCallVideoBitrateRange(): CallVideoBitrateRange = runBlocking { configFiles.submit("load call video bitrate") { videoBitrateRange(context.dataStore.data.first()) }.await() } @@ -316,7 +333,7 @@ class ConfigProvider(private val context: Context) { "debug.console_level" -> prefs[debugConsoleLevel] ?: "info" "debug.log_file" -> prefs[debugLogFile] ?: "utun.log" "debug.categories" -> prefs[debugCategories] ?: "" - "log_udp.ip" -> prefs[logUdpIp] ?: "192.168.40.250" + "log_udp.ip" -> prefs[logUdpIp] ?: UdpLogSettings.DEFAULT_IP else -> null } @@ -329,7 +346,8 @@ class ConfigProvider(private val context: Context) { "gui.utun_enabled" -> prefs[guiUtunEnabled] ?: 0 "node.listen_port" -> prefs[nodeListenPort] ?: 12345 "control.port" -> prefs[controlPort] ?: 9999 - "log_udp.port" -> prefs[logUdpPort] ?: 9999 + "log_udp.enabled" -> if (prefs[logUdpEnabled] ?: false) 1 else 0 + "log_udp.port" -> prefs[logUdpPort] ?: UdpLogSettings.DEFAULT_PORT "chat.storage_autoload" -> prefs[chatStorageAutoload] ?: 1 "chat.storage_autoload_maxsize_mb" -> prefs[chatStorageAutoloadMaxsizeMb] ?: 100 "chat.storage_maxsize_gb" -> prefs[chatStorageMaxsizeGb] ?: 1 @@ -376,6 +394,7 @@ class ConfigProvider(private val context: Context) { "debug.categories" -> prefs[debugCategories] = value "log_udp.ip" -> prefs[logUdpIp] = value "log_udp.port" -> prefs[logUdpPort] = value.toIntOrNull() ?: 0 + "log_udp.enabled" -> prefs[logUdpEnabled] = value == "1" || value == "yes" || value == "true" "chat.storage_autoload" -> prefs[chatStorageAutoload] = value.toIntOrNull() ?: 1 "chat.storage_autoload_maxsize_mb" -> prefs[chatStorageAutoloadMaxsizeMb] = value.toIntOrNull() ?: 100 "chat.storage_maxsize_gb" -> prefs[chatStorageMaxsizeGb] = value.toIntOrNull() ?: 1 @@ -881,14 +900,10 @@ class ConfigProvider(private val context: Context) { if (trimmed.isNotEmpty()) appendLine(trimmed) } } - val udpIp = stringValue("log_udp.ip", prefs, snapshot.keys) ?: "" - val udpPort = intValue("log_udp.port", prefs) - if (udpIp.isNotEmpty() && udpPort > 0) { - appendLine() - appendLine("[log_udp]") - appendLine("ip=$udpIp") - appendLine("port=$udpPort") - } + val udpLog = udpLogSettings(prefs) + udpLog.appendConfig(this) + LogManager.addLog(if (udpLog.enabled && !udpLog.active) "WARN" else "INFO", "UDPLog", + "Startup network logging enabled=${udpLog.enabled} active=${udpLog.active} target=${udpLog.ip}:${udpLog.port}") } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UdpLogSettings.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UdpLogSettings.kt new file mode 100644 index 00000000..f6ab06e0 --- /dev/null +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UdpLogSettings.kt @@ -0,0 +1,24 @@ +package com.utun.chat.data + +/** Адрес logreceiver сохраняется при выключении; наличие адреса само по себе не включает отправку. */ +data class UdpLogSettings(val enabled: Boolean = false, val ip: String = DEFAULT_IP, val port: Int = DEFAULT_PORT) { + val validAddress: Boolean get() = ip.split('.').let { parts -> parts.size == 4 && parts.all { part -> + part.toIntOrNull()?.let { it in 0..255 && part == it.toString() } == true + } } + val active: Boolean get() = enabled && validAddress && port in 1..65535 + + /** Добавить сетевой target в INI только при явном включении и заполненном адресе/порте. */ + fun appendConfig(config: StringBuilder) { + if (active) { + config.appendLine() + config.appendLine("[log_udp]") + config.appendLine("ip=$ip") + config.appendLine("port=$port") + } + } + + companion object { + const val DEFAULT_IP = "192.168.40.250" + const val DEFAULT_PORT = 9999 + } +} diff --git a/tools/chatgui-android/app/src/test/java/com/utun/chat/data/UdpLogSettingsTest.kt b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/UdpLogSettingsTest.kt new file mode 100644 index 00000000..682a551b --- /dev/null +++ b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/UdpLogSettingsTest.kt @@ -0,0 +1,40 @@ +package com.utun.chat.data + +import org.junit.Assert.* +import org.junit.Test + +class UdpLogSettingsTest { + private fun config(settings: UdpLogSettings): String = buildString { appendLine("[global]"); settings.appendConfig(this) } + + @Test fun defaultAddressDoesNotEnableNetworkLogging() { + val settings = UdpLogSettings() + assertFalse(settings.enabled) + assertFalse(settings.active) + assertEquals("[global]\n", config(settings)) + } + + @Test fun explicitEnableWritesTargetIntoStartupConfig() { + val settings = UdpLogSettings(true, "192.168.40.250", 9999) + assertTrue(settings.active) + assertEquals("[global]\n\n[log_udp]\nip=192.168.40.250\nport=9999\n", config(settings)) + } + + @Test fun disabledTargetRetainsAddressWithoutConfiguringSender() { + val enabled = UdpLogSettings(true, "10.20.30.40", 12345) + val disabled = enabled.copy(enabled = false) + assertEquals(enabled.ip, disabled.ip) + assertEquals(enabled.port, disabled.port) + assertEquals("[global]\n", config(disabled)) + assertEquals(config(enabled), config(disabled.copy(enabled = true))) + } + + @Test fun malformedTargetsNeverEnableSender() { + for (ip in listOf("", "localhost", "127.1", "1.2.3.256", "01.2.3.4", "1.2.3.4 ")) { + val settings = UdpLogSettings(true, ip, 9999) + assertFalse(ip, settings.active) + assertEquals("[global]\n", config(settings)) + } + for (port in listOf(-1, 0, 65536)) assertFalse(UdpLogSettings(true, "127.0.0.1", port).active) + assertTrue(UdpLogSettings(true, "127.0.0.1", 65535).active) + } +}