Files
wz-phone/docs/android/debugging.md
Claude 8d5f6fe044 feat: wire QUIC transport, JNI bridge, connect UI + add docs
- 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>
2026-04-05 04:43:49 +00:00

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:

  1. Relay not running: Verify the relay is listening:

    nc -zvu 172.16.81.125 4433
    
  2. Wrong relay address: Hardcoded in CallViewModel.kt:

    const val DEFAULT_RELAY = "172.16.81.125:4433"
    
  3. QUIC blocked by firewall: QUIC uses UDP. Many networks block UDP traffic. Ensure UDP port 4433 is open.

  4. 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

  1. Microphone permission denied: Check Android settings. The app requests RECORD_AUDIO on first launch.

  2. Oboe failed to start: The codec thread logs this. Check logcat for "failed to start audio".

  3. Ring buffer underrun: The stats overlay shows "Under" count. High underruns mean the codec thread isn't keeping up.

  4. 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_farend is 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_depth in stats (should be > 0 during active call)
  • underruns counter (should not climb rapidly)
  • Network jitter (high jitter_ms causes adaptation)

No sound from speaker

  1. Check isSpeaker state in the UI
  2. Oboe playout stream may have failed — check logcat for Oboe errors
  3. Ring buffer might be empty — check framesDecoded counter

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:

  1. APK installed for correct ABI? (arm64-v8a only)
  2. Manifest class names fully qualified? (no dots prefix)
  3. Relay running and reachable? (nc -zvu <host> <port>)
  4. Microphone permission granted?
  5. Stats polling working? (check if frame counters increment)
  6. Logcat for native crashes? (adb logcat -s DEBUG:V)
  7. Network connectivity? (UDP port open, no firewall)