Files
wz-phone/docs/android/maintenance.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.9 KiB

Maintenance Guide

Code Map — Where to Change Things

Changing the relay address or room

Edit CallViewModel.kt:

companion object {
    const val DEFAULT_RELAY = "172.16.81.125:4433"
    const val DEFAULT_ROOM = "android"
}

For a proper settings screen, add a new Composable in ui/ that persists to SharedPreferences and passes values to viewModel.startCall(relay, room).

Adding authentication

  1. In CallViewModel.startCall(), pass a token parameter
  2. In engine.rs, after QUIC connect but before CallOffer, send:
    transport.send_signal(&SignalMessage::AuthToken { token: auth_token }).await?;
    
  3. Wait for the relay to accept before proceeding to handshake
  4. Start relay with --auth-url <featherchat-endpoint>

Enabling media encryption

The crypto session is already derived in engine.rs but not applied to packets. To enable:

  1. Pass _session (currently unused) to the send/recv tasks
  2. Before transport.send_media(), encrypt the payload:
    let mut ciphertext = Vec::new();
    session.encrypt(&header_bytes, &payload, &mut ciphertext)?;
    packet.payload = Bytes::from(ciphertext);
    
  3. After transport.recv_media(), decrypt:
    let mut plaintext = Vec::new();
    session.decrypt(&header_bytes, &pkt.payload, &mut plaintext)?;
    pkt.payload = Bytes::from(plaintext);
    

Adding a new codec / quality profile

  1. Define the profile in wzp-proto/src/codec_id.rs
  2. Implement AudioEncoder/AudioDecoder traits in wzp-codec
  3. Register in AdaptiveEncoder/AdaptiveDecoder switch logic
  4. Add to supported_profiles in the CallOffer (engine.rs)

Changing audio parameters

  • Sample rate: Change FRAME_SAMPLES in audio_android.rs and WzpOboeConfig.sample_rate in oboe_bridge.cpp. Must match the codec's expected rate.
  • Frame duration: Change FRAME_SAMPLES (960 = 20ms at 48kHz, 1920 = 40ms)
  • Ring buffer size: Change RING_CAPACITY in audio_android.rs
  • AEC tail length: Change the 100 in Pipeline::new()EchoCanceller::new(48000, 100)

Adding x86_64 support (emulator)

  1. build.gradle.kts: add "x86_64" to abiFilters
  2. cargoNdkBuild task: add -t x86_64
  3. build.rs: handle x86_64-linux-android target for Oboe
  4. Note: Oboe in the emulator uses a different audio HAL — audio quality will differ

Dependency Overview

Rust Crate Dependencies (wzp-android)

Crate Version Purpose Upgrade risk
jni 0.21 Java FFI Low — stable API
tokio 1.x Async runtime Low
quinn 0.11 QUIC transport Medium — breaking changes between 0.x
rustls 0.23 TLS for QUIC Medium — tied to quinn version
serde_json 1.x Stats serialization Low
anyhow 1.x Error handling Low
tracing 0.1 Logging Low
rand 0.8 Random seed generation Low

Workspace Crate Dependencies

Crate Purpose Key trait
wzp-proto Shared types and traits MediaTransport, AudioEncoder, KeyExchange
wzp-codec Opus + Codec2 + signal processing AdaptiveEncoder, EchoCanceller
wzp-fec RaptorQ FEC RaptorQFecEncoder
wzp-crypto Key exchange + encryption WarzoneKeyExchange, ChaChaSession
wzp-transport QUIC connection management QuinnTransport, connect()

Android/Kotlin Dependencies

Library Version Purpose
compose-bom 2024.01.00 Compose version alignment
material3 (from BOM) UI components
activity-compose 1.8.2 Activity integration
lifecycle-runtime-ktx 2.7.0 ViewModel + coroutines
core-ktx 1.12.0 Kotlin extensions

Updating Dependencies

Rust

cargo update -p wzp-android
cargo ndk -t arm64-v8a build --release -p wzp-android

Watch for quinn/rustls version coupling. They must be compatible:

  • quinn 0.11 requires rustls 0.23

Android/Kotlin

Update versions in android/app/build.gradle.kts. Key compatibility:

  • kotlinCompilerExtensionVersion must match the Kotlin version
  • compose-bom version determines all Compose library versions
  • compileSdk and targetSdk should stay in sync

NDK

If upgrading the NDK:

  1. Update ndkVersion in build.gradle.kts
  2. Update ANDROID_NDK_HOME environment variable
  3. Update CC_aarch64_linux_android and friends
  4. Verify Oboe still builds with the new toolchain

Key Invariants to Preserve

  1. JNI function names must match package structure: If the Kotlin package changes, all Java_com_wzp_engine_WzpEngine_* functions in jni_bridge.rs must be renamed.

  2. Manifest uses fully-qualified class names: Never use .ClassName shorthand because the Gradle namespace (com.wzp.phone) differs from the Kotlin package (com.wzp).

  3. Stats JSON field names are snake_case: Rust serializes with serde defaults (snake_case). Kotlin's CallStats.fromJson() expects duration_secs, loss_pct, etc.

  4. Ring buffer ordering: Producer uses Release store on write index, consumer uses Acquire load. Breaking this causes torn reads.

  5. Codec thread owns Pipeline: Pipeline is !Send (Opus encoder state). It must never be accessed from another thread.

  6. panic::catch_unwind on all JNI functions: Rust panics unwinding across the FFI boundary is UB. Every JNI-exposed function must catch panics.

  7. Channel capacity (64): Both send_tx and recv_tx are bounded at 64 packets. If the network is slow, packets are dropped (try_send best-effort).

Testing

Unit Tests (Rust)

# Run all workspace tests (host, not Android)
cargo test

# Run only wzp-android tests (uses oboe_stub.cpp on host)
cargo test -p wzp-android

Note: Pipeline, codec, FEC, crypto tests run on the host. Audio tests use stubs.

On-Device Testing

  1. Build and install debug APK
  2. Open app, tap CALL
  3. Verify in logcat:
    • WzpEngine created via JNI
    • connecting to relay...
    • QUIC connected to relay
    • CallOffer sent
    • handshake complete, call active
    • codec thread started
  4. Check stats overlay: frame counters should increment
  5. Speak into mic — other connected device should hear audio

Stress Testing

  • Run a call for 30+ minutes — check for memory leaks (stats should be stable)
  • Kill and restart the relay — client should eventually get a connection error
  • Toggle mute rapidly — verify no crashes
  • Switch speaker on/off — verify audio route changes

Performance Monitoring

Key metrics to watch during a call:

Metric Healthy Range Warning Critical
frames_encoded Increasing ~50/sec Stalled 0
frames_decoded Increasing ~50/sec Stalled 0
underruns < 5/min > 20/min > 100/min
jitter_buffer_depth 2-5 0 or >10 N/A
loss_pct < 5% 5-20% > 20%
rtt_ms < 100ms 100-300ms > 500ms