chat: user-friendly INFO logs for invite/connect flow
- chat_sync.c: invite entry, JOINED/TIMEOUT messages - conn_mgr_core.c: invite start (addrs+udp+tcp), NCD UP/DOWN/TIMEOUT, TCP connect/ready/close, membership check send/receive/confirm/deny, overall timeout - node_conn_direct.c: NCD event names instead of numbers
This commit is contained in:
1 parent
c35135cb52
commit
c2191aad3b
34 files changed
+597
-230
No files matched your search
@@ -3,6 +3,47 @@ package com.utun.chat.data
|
||||
import android.util.Log
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
const val EVT_NODEINFO_UPDATED = 16
|
||||
|
||||
data class NodeStatus(
|
||||
val nodeId: Long = 0,
|
||||
val directPresence: Boolean = false,
|
||||
val directUp: Boolean = false,
|
||||
val indirectPresence: Boolean = false,
|
||||
val indirectUp: Boolean = false,
|
||||
val bgpPresence: Boolean = false,
|
||||
val bgpUp: Boolean = false,
|
||||
val bestRtt: Int = 0,
|
||||
val rttType: Int = 0 // 0=interface, 1=nat, 2=real
|
||||
) {
|
||||
val online: Boolean get() = (directPresence && directUp) || (indirectPresence && indirectUp)
|
||||
val hasPresence: Boolean get() = directPresence || indirectPresence || bgpPresence
|
||||
}
|
||||
|
||||
fun parseNodeStatus(data: ByteArray): NodeStatus? {
|
||||
if (data.size < 12) return null
|
||||
val buf = ByteBuffer.wrap(data, 0, 12).order(ByteOrder.LITTLE_ENDIAN)
|
||||
val nodeId = buf.getLong()
|
||||
val presence = buf.get().toInt() and 0xFF
|
||||
val up = buf.get().toInt() and 0xFF
|
||||
val rttPacked = buf.getShort().toInt() and 0xFFFF
|
||||
val bestRtt = rttPacked and 0x3FFF
|
||||
val rttType = (rttPacked shr 14) and 3
|
||||
return NodeStatus(
|
||||
nodeId = nodeId,
|
||||
directPresence = (presence and 1) != 0,
|
||||
directUp = (up and 1) != 0,
|
||||
indirectPresence = (presence and 2) != 0,
|
||||
indirectUp = (up and 2) != 0,
|
||||
bgpPresence = (presence and 4) != 0,
|
||||
bgpUp = (up and 4) != 0,
|
||||
bestRtt = bestRtt,
|
||||
rttType = rttType
|
||||
)
|
||||
}
|
||||
|
||||
data class Channel(val id: String, val name: String, val lastMsgAt: Long = 0, val lastMsg: String = "",
|
||||
val peersOnline: Int = 0)
|
||||
@@ -15,11 +56,18 @@ data class Message(val id: Long, val author: String, val text: String, val ts: L
|
||||
data class ChatMember(val nodeId: Long, val name: String, val online: Boolean = false,
|
||||
val isAdmin: Boolean = false, val isSuper: Boolean = false,
|
||||
val isStorage: Boolean = false, val nodeType: Int = 0,
|
||||
val rtt: Int = 0, val isSelf: Boolean = false)
|
||||
val rtt: Int = 0, val rttType: Int = 0, val isSelf: Boolean = false,
|
||||
val directUp: Boolean = false, val indirectUp: Boolean = false,
|
||||
val directPresence: Boolean = false, val indirectPresence: Boolean = false,
|
||||
val bgpPresence: Boolean = false)
|
||||
|
||||
data class MemberDetail(val nodeId: Long, val name: String = "",
|
||||
val online: Boolean = false, val lastSeen: Long = 0,
|
||||
val created: Long = 0, val addresses: List<MemberAddr> = emptyList())
|
||||
val created: Long = 0, val addresses: List<MemberAddr> = emptyList(),
|
||||
val bestRtt: Int = 0, val rttType: Int = 0,
|
||||
val directPresence: Boolean = false, val directUp: Boolean = false,
|
||||
val indirectPresence: Boolean = false, val indirectUp: Boolean = false,
|
||||
val bgpPresence: Boolean = false)
|
||||
|
||||
data class MemberAddr(val family: Int = 0, val protocol: String = "", val address: String = "",
|
||||
val port: Int = 0, val socketId: Int = 0, val rtt: Int = 0)
|
||||
|
||||
+58
-21
@@ -121,7 +121,17 @@ private fun MemberListItem(
|
||||
onClick: () -> Unit,
|
||||
onEdit: () -> Unit
|
||||
) {
|
||||
val onlineColor = if (member.online) Color(0xFF4CAF50) else Color(0xFF9E9E9E)
|
||||
val onlineColor = when {
|
||||
member.directUp || member.indirectUp -> Color(0xFF4CAF50) // green: up
|
||||
member.directPresence || member.indirectPresence || member.bgpPresence -> Color(0xFFFFA726) // orange: presence but down
|
||||
else -> Color(0xFF9E9E9E) // gray: offline
|
||||
}
|
||||
val connLabel = when {
|
||||
member.directUp -> "direct"
|
||||
member.indirectUp -> "indirect"
|
||||
member.directPresence || member.indirectPresence || member.bgpPresence -> "offline"
|
||||
else -> ""
|
||||
}
|
||||
val bgColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent
|
||||
|
||||
Surface(
|
||||
@@ -152,27 +162,22 @@ private fun MemberListItem(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = if (member.isSelf) Color(0xFF4CAF50) else Color.Unspecified
|
||||
)
|
||||
val roles = buildList {
|
||||
if (member.isAdmin) add("Admin")
|
||||
if (member.isSuper) add("Super")
|
||||
if (member.isStorage) add("Storage")
|
||||
}
|
||||
if (roles.isNotEmpty() || member.rtt > 0) {
|
||||
val parts = mutableListOf<String>()
|
||||
if (member.isAdmin) parts.add("Admin")
|
||||
if (member.isSuper) parts.add("Super")
|
||||
if (member.isStorage) parts.add("Storage")
|
||||
if (member.directUp) parts.add("direct↑")
|
||||
else if (member.indirectUp) parts.add("indirect↑")
|
||||
else if (member.directPresence || member.indirectPresence || member.bgpPresence) parts.add("down")
|
||||
if (member.rtt > 0) parts.add("${member.rtt}ms")
|
||||
if (parts.isNotEmpty()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (roles.isNotEmpty())
|
||||
Text(
|
||||
text = roles.joinToString(", "),
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
if (member.rtt > 0) {
|
||||
if (roles.isNotEmpty()) Text(" ", fontSize = 11.sp)
|
||||
Text(
|
||||
text = "${member.rtt / 10}ms",
|
||||
fontSize = 11.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = parts.joinToString(" "),
|
||||
fontSize = 11.sp,
|
||||
color = if (member.directUp || member.indirectUp)
|
||||
Color(0xFF4CAF50) else Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,6 +271,38 @@ private fun MemberDetailCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
val rttTypeName = when (detail.rttType) { 0 -> "interface" 1 -> "nat" 2 -> "real" else -> "?" }
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text("Connection:", fontSize = 13.sp, fontWeight = FontWeight.Medium)
|
||||
if (detail.directPresence) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.size(8.dp).clip(CircleShape)
|
||||
.background(if (detail.directUp) Color(0xFF4CAF50) else Color(0xFFE53935)))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text("Direct: ${if (detail.directUp) "UP" else "DOWN"}" +
|
||||
(if (detail.directUp && detail.bestRtt > 0) " ${detail.bestRtt}ms ($rttTypeName)" else ""),
|
||||
fontSize = 12.sp, color = Color.DarkGray)
|
||||
}
|
||||
}
|
||||
if (detail.indirectPresence) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.size(8.dp).clip(CircleShape)
|
||||
.background(if (detail.indirectUp) Color(0xFF4CAF50) else Color(0xFFE53935)))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text("Indirect: ${if (detail.indirectUp) "UP" else "DOWN"}",
|
||||
fontSize = 12.sp, color = Color.DarkGray)
|
||||
}
|
||||
}
|
||||
if (detail.bgpPresence) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.size(8.dp).clip(CircleShape).background(Color(0xFF4CAF50)))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text("BGP: visible", fontSize = 12.sp, color = Color.DarkGray)
|
||||
}
|
||||
}
|
||||
if (!detail.directPresence && !detail.indirectPresence && !detail.bgpPresence) {
|
||||
Text("offline", fontSize = 12.sp, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ class ChatViewModel : ViewModel() {
|
||||
private val _isStuck = MutableStateFlow(false)
|
||||
val isStuck: StateFlow<Boolean> = _isStuck
|
||||
|
||||
private val _nodeStatuses = MutableStateFlow<Map<Long, NodeStatus>>(emptyMap())
|
||||
val nodeStatuses: StateFlow<Map<Long, NodeStatus>> = _nodeStatuses
|
||||
|
||||
val audioRecorder = AudioRecorderManager()
|
||||
|
||||
private val _isRecording = MutableStateFlow(false)
|
||||
@@ -186,6 +189,13 @@ class ChatViewModel : ViewModel() {
|
||||
4 -> if (repo != null) refreshChannels() /* CHANNEL_UPDATED */
|
||||
12 -> LogManager.addLog("INFO", "VM", "service started")
|
||||
13 -> LogManager.addLog("INFO", "VM", "service stopped")
|
||||
16 -> { /* NODEINFO_UPDATED: [node_id:8][presence:1][up:1][best_rtt:2] */
|
||||
if (data == null || data.size < 12) return
|
||||
val ns = parseNodeStatus(data) ?: return
|
||||
_nodeStatuses.value = _nodeStatuses.value + (ns.nodeId to ns)
|
||||
val cur = _currentChannel.value
|
||||
if (cur != null) refreshMembers(cur.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,15 +238,33 @@ class ChatViewModel : ViewModel() {
|
||||
|
||||
fun refreshMembers(chId: String) {
|
||||
val r = repo ?: return
|
||||
val statuses = _nodeStatuses.value
|
||||
viewModelScope.launch {
|
||||
_members.value = withContext(Dispatchers.IO) { r.getMembers(chId) }
|
||||
val raw = withContext(Dispatchers.IO) { r.getMembers(chId) }
|
||||
_members.value = raw.map { m ->
|
||||
val ns = statuses[m.nodeId]
|
||||
if (ns != null) m.copy(
|
||||
online = ns.online, rtt = ns.bestRtt, rttType = ns.rttType,
|
||||
directUp = ns.directUp, indirectUp = ns.indirectUp,
|
||||
directPresence = ns.directPresence, indirectPresence = ns.indirectPresence,
|
||||
bgpPresence = ns.bgpPresence
|
||||
) else m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showMemberDetail(channelId: String, nodeId: Long) {
|
||||
val r = repo ?: return
|
||||
val ns = _nodeStatuses.value[nodeId]
|
||||
viewModelScope.launch {
|
||||
_selectedMember.value = withContext(Dispatchers.IO) { r.getMemberDetail(channelId, nodeId) }
|
||||
val d = withContext(Dispatchers.IO) { r.getMemberDetail(channelId, nodeId) }
|
||||
if (d != null && ns != null) _selectedMember.value = d.copy(
|
||||
online = ns.online, bestRtt = ns.bestRtt, rttType = ns.rttType,
|
||||
directPresence = ns.directPresence, directUp = ns.directUp,
|
||||
indirectPresence = ns.indirectPresence, indirectUp = ns.indirectUp,
|
||||
bgpPresence = ns.bgpPresence
|
||||
)
|
||||
else _selectedMember.value = d
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user