diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt index d499dbe7..d585cc9d 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt @@ -123,8 +123,19 @@ data class MemberLinks(val connected: Boolean = false, val initialized: Boolean data class LocalSocketState( val sockId: Int = 0, val ifName: String = "", val bindIp: String = "", val port: Int = 0, val protocol: String = "", val state: Int = 0, - val linksUp: Int = 0, val linksDown: Int = 0 + val linksUp: Int = 0, val linksDown: Int = 0, + val natMapping: Int = 0, val natInbound: Int = 0, val natSource: Int = 0, + val externalIp: String = "", val externalPort: Int = 0 ) { + val natLabelRes: Int get() = when { + natSource == 0 -> R.string.socket_nat_unmeasured + natMapping == 1 -> R.string.socket_no_nat + natMapping == 2 && natInbound == 1 -> R.string.socket_eim_open + natMapping == 2 && natInbound == 2 -> R.string.socket_eim_filtered + natMapping == 2 -> R.string.address_nat_eim_unverified + natMapping == 3 -> R.string.address_nat_dependent + else -> R.string.address_nat_unknown + } val stateLabel: String get() = AppLanguage.text(when (state) { 2 -> R.string.connection_up; 1 -> R.string.connection_connecting; else -> R.string.connection_down }) @@ -543,9 +554,9 @@ class ChatRepository { return MemberLinks() } - fun getLocalSockets(): List { + fun parseLocalSockets(json: String): List { try { - val arr = JSONArray(NativeLib.getLocalSockets()) + val arr = JSONArray(json) val list = mutableListOf() for (i in 0 until arr.length()) { val obj = arr.getJSONObject(i) @@ -559,12 +570,17 @@ class ChatRepository { protocol = obj.optString("pt", ""), state = obj.optInt("st", 0), linksUp = obj.optInt("lu", 0), - linksDown = obj.optInt("ld", 0) + linksDown = obj.optInt("ld", 0), + natMapping = obj.getInt("nmapping"), + natInbound = obj.getInt("ninbound"), + natSource = obj.getInt("nsource"), + externalIp = obj.getString("external"), + externalPort = obj.getInt("externalPort") )) } list.sortWith(compareBy { it.ifName }.thenBy { it.bindIp }.thenBy { it.protocol }.thenBy { it.port }) return list - } catch (e: Exception) { Log.w("utun-gui", "getLocalSockets error", e) } + } catch (e: Exception) { Log.w("utun-gui", "local sockets snapshot parse error", e) } return emptyList() } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index 12cbe8ef..240173db 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt @@ -169,7 +169,7 @@ object NativeLib { fun getMemberLinks(nodeId: Long): String = nativeGetMemberLinks(nodeId) ?: "{}" /* ── Local sockets (for auto-mode display) ── */ - fun getLocalSockets(): String = nativeGetLocalSockets() ?: "[]" + fun requestLocalSockets() = nativeRequestLocalSockets() /* ── Voice recording ── */ fun voiceStart(channelId: String, replyTo: String = "", requestId: Long = 0): Boolean = @@ -364,7 +364,7 @@ object NativeLib { private external fun nativeGetMemberLinks(nodeId: Long): String? /* ── Local sockets JNI ── */ - private external fun nativeGetLocalSockets(): String? + private external fun nativeRequestLocalSockets() /* ── Voice recording JNI ── */ private external fun nativeVoiceStart(channelId: String, replyTo: ByteArray, requestId: Long): Boolean diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index a7e7dfea..c0081310 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt @@ -675,7 +675,10 @@ class ChatViewModel : ViewModel() { LogManager.addLog("ERROR", "stream", "source snapshot parse group=$group: ${e.message}") } } - 19 -> { if (repo != null) refreshLocalSockets() } /* LOCAL_SOCKETS */ + 19 -> { /* LOCAL_SOCKETS: снимок создан в потоке ядра; пустое событие просит обновить его. */ + if (data == null || data.isEmpty()) refreshLocalSockets() + else repo?.let { _localSockets.value = it.parseLocalSockets(String(data, Charsets.UTF_8)) } + } 23 -> { /* MEMBER_LIST: live-снимок дополняет расширенный JSON-список. */ if (data == null || data.size < 3) { LogManager.addLog("ERROR", "VM", "MEMBER_LIST: missing header"); return @@ -1487,10 +1490,7 @@ class ChatViewModel : ViewModel() { } fun refreshLocalSockets() { - val r = repo ?: return - viewModelScope.launch { - _localSockets.value = withContext(Dispatchers.IO) { r.getLocalSockets() } - } + if (repo != null) NativeLib.requestLocalSockets() } fun sendMessage(text: String) {