feat: pure Kotlin UDP ping — periodic every 5s, no JNI crash
Replace WzpEngine.pingRelay() (JNI, loads native .so, crashes jemalloc on Android 16 MTE) with pure Kotlin DatagramSocket UDP probe. - RelayPinger: sends QUIC Version Negotiation trigger packet, measures RTT from response. No native lib, no JNI, zero crash risk. - Periodic: pings all servers every 5 seconds via coroutine - Server fingerprint: filled lazily on first real QUIC connection (TOFU still works, just delayed) - Lock status: OFFLINE when ping fails, NEW until first connection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -203,38 +203,46 @@ class CallViewModel : ViewModel(), WzpCallback {
|
||||
settings?.saveSelectedServer(_selectedServer.value)
|
||||
}
|
||||
|
||||
/** Ping all servers in background, update results. */
|
||||
fun pingAllServers() {
|
||||
private var pingJob: Job? = null
|
||||
|
||||
/** Start periodic ping every 5 seconds. Safe to call multiple times. */
|
||||
fun startPeriodicPing() {
|
||||
if (pingJob?.isActive == true) return
|
||||
pingJob = viewModelScope.launch {
|
||||
while (isActive) {
|
||||
pingAllServersOnce()
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop periodic ping. */
|
||||
fun stopPeriodicPing() {
|
||||
pingJob?.cancel()
|
||||
pingJob = null
|
||||
}
|
||||
|
||||
/** Ping all servers once (pure Kotlin UDP, no JNI). */
|
||||
fun pingAllServersOnce() {
|
||||
viewModelScope.launch {
|
||||
val results = mutableMapOf<String, PingResult>()
|
||||
val known = mutableMapOf<String, String>()
|
||||
_servers.value.forEach { server ->
|
||||
val pr = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val json = WzpEngine.pingRelay(server.address) ?: return@withContext null
|
||||
val obj = JSONObject(json)
|
||||
PingResult(
|
||||
rttMs = obj.getInt("rtt_ms"),
|
||||
serverFingerprint = obj.optString("server_fingerprint", ""),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "ping ${server.address} failed: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
if (pr != null) {
|
||||
results[server.address] = pr
|
||||
// TOFU: save fingerprint on first contact
|
||||
if (pr.serverFingerprint.isNotEmpty()) {
|
||||
val saved = settings?.loadServerFingerprint(server.address)
|
||||
if (saved == null) {
|
||||
settings?.saveServerFingerprint(server.address, pr.serverFingerprint)
|
||||
}
|
||||
known[server.address] = saved ?: pr.serverFingerprint
|
||||
}
|
||||
val udpResult = withContext(Dispatchers.IO) {
|
||||
com.wzp.net.RelayPinger.ping(server.address)
|
||||
}
|
||||
results[server.address] = PingResult(
|
||||
rttMs = udpResult.rttMs,
|
||||
serverFingerprint = "", // filled lazily on first real connection
|
||||
)
|
||||
}
|
||||
_pingResults.value = results
|
||||
// Load saved TOFU fingerprints
|
||||
val known = mutableMapOf<String, String>()
|
||||
_servers.value.forEach { server ->
|
||||
settings?.loadServerFingerprint(server.address)?.let {
|
||||
known[server.address] = it
|
||||
}
|
||||
}
|
||||
_knownFingerprints.value = known
|
||||
}
|
||||
}
|
||||
@@ -242,9 +250,9 @@ class CallViewModel : ViewModel(), WzpCallback {
|
||||
/** Get lock status for a server. */
|
||||
fun lockStatus(address: String): LockStatus {
|
||||
val pr = _pingResults.value[address] ?: return LockStatus.UNKNOWN
|
||||
val known = _knownFingerprints.value[address]
|
||||
if (pr.serverFingerprint.isEmpty()) return LockStatus.NEW
|
||||
if (known == null) return LockStatus.NEW
|
||||
if (pr.rttMs <= 0 && pr.serverFingerprint.isEmpty()) return LockStatus.OFFLINE
|
||||
val known = _knownFingerprints.value[address] ?: return LockStatus.NEW
|
||||
if (pr.serverFingerprint.isEmpty()) return LockStatus.NEW // no fingerprint yet
|
||||
return if (pr.serverFingerprint == known) LockStatus.VERIFIED else LockStatus.CHANGED
|
||||
}
|
||||
|
||||
|
||||
@@ -90,8 +90,10 @@ fun InCallScreen(
|
||||
|
||||
var showManageRelays by remember { mutableStateOf(false) }
|
||||
|
||||
// Don't auto-ping — loading the native .so triggers jemalloc init
|
||||
// which crashes on Android 16 MTE. Let user click "Ping All" manually.
|
||||
// Pure Kotlin UDP ping — no native .so loading, safe on Android 16 MTE
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.startPeriodicPing()
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
||||
Reference in New Issue
Block a user