- 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>
215 lines
6.2 KiB
Markdown
215 lines
6.2 KiB
Markdown
# 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:
|
|
|
|
```xml
|
|
<!-- 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:
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```bash
|
|
adb logcat -s wzp-android:V wzp-codec:V wzp-net:V
|
|
```
|
|
|
|
### View Rust tracing output (if android_logger is added)
|
|
|
|
```bash
|
|
adb logcat | grep -E "(wzp|WzpEngine|CallActivity)"
|
|
```
|
|
|
|
### View Oboe audio logs
|
|
|
|
```bash
|
|
adb logcat -s AAudio:V oboe:V
|
|
```
|
|
|
|
### View native crashes
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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:
|
|
```bash
|
|
nc -zvu 172.16.81.125 4433
|
|
```
|
|
|
|
2. **Wrong relay address**: Hardcoded in `CallViewModel.kt`:
|
|
```kotlin
|
|
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:
|
|
|
|
```json
|
|
{
|
|
"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)
|