- Replace raw FFI with proper `jni` crate for string marshalling - Wire QUIC transport in engine: connect to relay, crypto handshake (CallOffer/CallAnswer, X25519+Ed25519), send/recv MediaPackets - Feed received packets into jitter buffer (was previously ignored) - Add connect screen UI with CALL button (idle state) and in-call controls (mute, speaker, hang up, live stats) - Hardcode relay 172.16.81.125:4433, room "android" - Add comprehensive docs in docs/android/: architecture.md (8 mermaid diagrams), build-guide.md, debugging.md, maintenance.md, roadmap.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6.2 KiB
Debugging Guide
Crash on Launch
Symptom: App crashes immediately after opening
Most likely cause: Namespace mismatch in AndroidManifest.xml
The Gradle namespace is com.wzp.phone but all Kotlin classes are in package com.wzp.*. If the manifest uses shorthand names (.WzpApplication, .ui.call.CallActivity), Android resolves them as com.wzp.phone.WzpApplication which doesn't exist.
Fix: Always use fully-qualified class names in the manifest:
<!-- WRONG -->
<application android:name=".WzpApplication">
<activity android:name=".ui.call.CallActivity">
<!-- CORRECT -->
<application android:name="com.wzp.WzpApplication">
<activity android:name="com.wzp.ui.call.CallActivity">
Symptom: Crash in System.loadLibrary("wzp_android")
The native .so is missing or incompatible. Check:
# Verify the .so exists in the APK
unzip -l app-release.apk | grep libwzp
# Should show: lib/arm64-v8a/libwzp_android.so
# Verify ABI matches device
adb shell getprop ro.product.cpu.abi
# Should return: arm64-v8a
Symptom: Crash when calling nativeGetStats() (returns null jstring)
The JNI bridge must return a valid jstring, not a null pointer. The Kotlin side declares the return as String? (nullable) and wraps in try/catch:
fun getStats(): String {
if (nativeHandle == 0L) return "{}"
return try {
nativeGetStats(nativeHandle) ?: "{}"
} catch (_: Exception) {
"{}"
}
}
Symptom: Tracing subscriber panic
tracing_subscriber::fmt() writes to stdout, which doesn't exist on Android. The init was removed. If you need logging, use android_logger crate instead.
Logcat Filters
View all WZP logs
adb logcat -s wzp-android:V wzp-codec:V wzp-net:V
View Rust tracing output (if android_logger is added)
adb logcat | grep -E "(wzp|WzpEngine|CallActivity)"
View Oboe audio logs
adb logcat -s AAudio:V oboe:V
View native crashes
adb logcat -s DEBUG:V libc:V
Look for signal 11 (SIGSEGV) or signal 6 (SIGABRT) with a backtrace in libwzp_android.so.
Symbolicate native crash
# Find the .so with debug symbols (before stripping)
SO_PATH="target/aarch64-linux-android/release/libwzp_android.so"
# Use addr2line from NDK
$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-addr2line \
-e $SO_PATH -f 0x<address_from_crash>
Network Issues
Call stuck on "Connecting..."
The QUIC handshake to the relay is failing. Common causes:
-
Relay not running: Verify the relay is listening:
nc -zvu 172.16.81.125 4433 -
Wrong relay address: Hardcoded in
CallViewModel.kt:const val DEFAULT_RELAY = "172.16.81.125:4433" -
QUIC blocked by firewall: QUIC uses UDP. Many networks block UDP traffic. Ensure UDP port 4433 is open.
-
TLS handshake failure: The client uses
client_config()which disables certificate verification. If the relay's QUIC config changed, this may fail.
Connected but no audio
-
Microphone permission denied: Check Android settings. The app requests
RECORD_AUDIOon first launch. -
Oboe failed to start: The codec thread logs this. Check logcat for "failed to start audio".
-
Ring buffer underrun: The stats overlay shows "Under" count. High underruns mean the codec thread isn't keeping up.
-
Network not forwarding: If both phones show "Active" but frame counters aren't increasing, the relay may not be forwarding. Check relay logs.
High packet loss
The stats overlay shows loss percentage. Common causes:
- Wi-Fi congestion (try cellular or move closer to AP)
- UDP throttling by carrier/ISP
- Relay overloaded (check relay metrics)
Audio Issues
Echo
AEC (Acoustic Echo Cancellation) is enabled by default with a 100ms tail. If echo persists:
- The AEC may need a longer tail for the specific acoustic environment
- Speaker volume too high overwhelms the canceller
- Check that
last_decoded_farendis being set (playout path working)
Robot voice / glitching
Usually caused by jitter buffer underruns. The jitter buffer adapts between 10-250 packets. Check:
jitter_buffer_depthin stats (should be > 0 during active call)underrunscounter (should not climb rapidly)- Network jitter (high jitter_ms causes adaptation)
No sound from speaker
- Check
isSpeakerstate in the UI - Oboe playout stream may have failed — check logcat for Oboe errors
- Ring buffer might be empty — check
framesDecodedcounter
JNI Issues
UnsatisfiedLinkError: No implementation found for...
The JNI function name doesn't match. JNI names must follow the pattern:
Java_com_wzp_engine_WzpEngine_<methodName>
If the package structure changes, all JNI function names must be updated in jni_bridge.rs.
Panic across FFI boundary
All JNI functions wrap their body in panic::catch_unwind(). If a Rust panic escapes to Java, it causes a SIGABRT. The catch_unwind returns safe defaults:
| Function | Panic return |
|---|---|
nativeInit |
0 (null handle) |
nativeStartCall |
-1 (error) |
nativeGetStats |
JObject::null() |
| Others | void (silently swallowed) |
Thread safety
All JNI methods must be called from the same thread (Android main thread). The EngineHandle is a raw pointer — concurrent access is undefined behavior.
Stats JSON Format
The nativeGetStats() returns JSON matching this Rust struct:
{
"state": "Active",
"duration_secs": 42.5,
"quality_tier": 0,
"loss_pct": 0.5,
"rtt_ms": 45,
"jitter_ms": 12,
"jitter_buffer_depth": 3,
"frames_encoded": 2125,
"frames_decoded": 2100,
"underruns": 5
}
Kotlin deserializes this via CallStats.fromJson() using org.json.JSONObject (Android built-in, no library needed).
Diagnostic Checklist
When something doesn't work, check in this order:
- APK installed for correct ABI? (
arm64-v8aonly) - Manifest class names fully qualified? (no dots prefix)
- Relay running and reachable? (
nc -zvu <host> <port>) - Microphone permission granted?
- Stats polling working? (check if frame counters increment)
- Logcat for native crashes? (
adb logcat -s DEBUG:V) - Network connectivity? (UDP port open, no firewall)