docs: update DESIGN, ARCHITECTURE, PRDs, PROGRESS for BT + network + build changes

Reflects the current reality: setCommunicationDevice API 31+, deferred
MODE_IN_COMMUNICATION, BT-mode Oboe (bt_active flag), per-arch builds,
Hangup call_id fix, and network monitoring integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-12 17:39:59 +04:00
parent 114d69e488
commit 300ea66d13
4 changed files with 88 additions and 19 deletions

View File

@@ -974,21 +974,33 @@ Cellular generation is approximated from `getLinkDownstreamBandwidthKbps()` to a
Both Android app variants support 3-way audio routing: **Earpiece → Speaker → Bluetooth SCO**.
### Audio Mode Lifecycle
`MODE_IN_COMMUNICATION` is set by the Rust call engine (via JNI `AudioManager.setMode()`) right before Oboe streams open — NOT at app launch. Restored to `MODE_NORMAL` when the call ends. This prevents hijacking system audio routing (music, BT A2DP) before a call is active.
### Native Kotlin App
`AudioRouteManager.kt` handles device detection (via `AudioDeviceCallback`), SCO lifecycle (`startBluetoothSco` / `stopBluetoothSco`), and auto-fallback on BT disconnect. `CallViewModel.cycleAudioRoute()` cycles through available routes.
`AudioRouteManager.kt` handles device detection (via `AudioDeviceCallback`), SCO lifecycle, and auto-fallback on BT disconnect. `CallViewModel.cycleAudioRoute()` cycles through available routes.
### Tauri Desktop App
`android_audio.rs` provides JNI bridges to `AudioManager` for speakerphone and Bluetooth SCO control. After each route change, Oboe streams are stopped and restarted via `spawn_blocking` to force AAudio to reconfigure with the new routing.
`android_audio.rs` provides JNI bridges to `AudioManager` for speakerphone and Bluetooth SCO control. After each route change, Oboe streams are stopped and restarted via `spawn_blocking`.
```
User tap ──► cycleAudioRoute()
├─ Earpiece: setSpeakerphoneOn(false)
├─ Earpiece: setSpeakerphoneOn(false) + clearCommunicationDevice()
├─ Speaker: setSpeakerphoneOn(true)
└─ BT SCO: startBluetoothSco() + setBluetoothScoOn(true)
└─ BT SCO: setCommunicationDevice(bt_device) [API 31+]
fallback: startBluetoothSco() [API < 31]
Oboe stop + start (~60-400ms)
Oboe stop + start_bt() for BT / start() for others
```
### BT SCO and Oboe
BT SCO only supports 8/16kHz. When `bt_active=1`, Oboe capture skips `setSampleRate(48000)` and `setInputPreset(VoiceCommunication)`, letting the system choose the native BT rate. Oboe's `SampleRateConversionQuality::Best` bridges to our 48kHz ring buffers. Playout uses `Usage::Media` in BT mode to avoid conflicts with the communication device routing.
### Hangup Signal Fix
`SignalMessage::Hangup` now carries an optional `call_id` field. The relay uses it to end only the specific call instead of broadcasting to all active calls for the user — preventing a race where a hangup for call 1 kills a newly-placed call 2.

View File

@@ -587,9 +587,19 @@ Signal messages are sent over reliable QUIC streams as length-prefixed JSON:
WarzonePhone supports three audio output routes on Android: **Earpiece**, **Speaker**, and **Bluetooth SCO**. The user cycles through available routes with a single button.
### Audio mode lifecycle
`MODE_IN_COMMUNICATION` is set **when the call engine starts** (right before Oboe `audio_start()`), not at app launch. This is critical — setting it early hijacks system audio routing (e.g. music drops from BT A2DP to earpiece). `MODE_NORMAL` is restored when the call engine stops.
```
App launch → MODE_NORMAL (other apps' audio unaffected)
Call start → set_audio_mode_communication() → MODE_IN_COMMUNICATION
Call end → audio_stop() → set_audio_mode_normal() → MODE_NORMAL
```
### Route lifecycle
1. Call starts → Earpiece (default). `AudioManager.MODE_IN_COMMUNICATION` set by `CallService`.
1. Call starts → Earpiece (default).
2. User taps route button → cycles to next available route.
3. Route change requires Oboe stream restart (~60-400ms) because AAudio silently tears down streams on some OEMs when the routing target changes mid-stream.
4. Bluetooth disconnect mid-call → `AudioDeviceCallback.onAudioDevicesRemoved` fires → auto-fallback to Earpiece or Speaker.
@@ -598,11 +608,13 @@ WarzonePhone supports three audio output routes on Android: **Earpiece**, **Spea
SCO (Synchronous Connection Oriented) is the correct Bluetooth profile for VoIP — it provides bidirectional mono audio at 8/16 kHz with ~30ms latency. A2DP (stereo, high-quality) is unidirectional and adds 100-200ms of buffering, making it unsuitable for real-time voice.
The deprecated `AudioManager.startBluetoothSco()` API is used because the modern `setCommunicationDevice()` requires API 31+ and our minSdk is 26. The deprecated APIs are functional on all tested devices through API 35.
On API 31+ (Android 12), we use the modern `setCommunicationDevice(AudioDeviceInfo)` API to route audio to the BT SCO device. The deprecated `startBluetoothSco()` + `setBluetoothScoOn()` path is used as fallback on older APIs. `setBluetoothScoOn()` is silently rejected on Android 12+ for non-system apps.
BT SCO devices only support 8/16kHz sample rates, but our pipeline runs at 48kHz. When BT is active, Oboe opens in **BT mode** (`bt_active=1`): capture skips `setSampleRate(48000)` and `setInputPreset(VoiceCommunication)`, letting the system open at the device's native rate. Oboe's `SampleRateConversionQuality::Best` resamples to/from 48kHz for our ring buffers.
### Two app variants
Both the native Kotlin app (`AudioRouteManager.kt`) and the Tauri app (`android_audio.rs` JNI bridge) call the same `AudioManager` APIs. The native app uses `AudioDeviceCallback` for automatic device detection; the Tauri app queries `getDevices()` on demand.
Both the native Kotlin app (`AudioRouteManager.kt`) and the Tauri app (`android_audio.rs` JNI bridge) support BT SCO routing. The native app uses `AudioDeviceCallback` for automatic device detection; the Tauri app uses `getAvailableCommunicationDevices()` (API 31+) or `getDevices()` on demand.
## Network Change Response
@@ -631,4 +643,19 @@ These thresholds are conservative. Carriers over-report bandwidth, but for VoIP
- **Rust** 1.85+ (2024 edition)
- **Linux**: cmake, pkg-config, libasound2-dev (for audio feature)
- **macOS**: Xcode command line tools (CoreAudio included)
- **Android**: NDK r27c, cmake 3.28+ (from pip)
- **Android**: NDK 26.1 (r26b), cmake 3.25-3.28 (system package)
### Android APK Builds
```bash
# arm64 only (default, 25MB release APK)
./scripts/build-tauri-android.sh --init --release --arch arm64
# armv7 only (smaller devices)
./scripts/build-tauri-android.sh --init --release --arch armv7
# both architectures as separate APKs
./scripts/build-tauri-android.sh --init --release --arch all
```
Release APKs are signed with `android/keystore/wzp-release.jks` via `apksigner`. Per-arch builds produce separate APKs (~25MB each vs ~50MB universal) for easier sharing with testers.

View File

@@ -50,18 +50,23 @@ Wire Bluetooth SCO routing end-to-end through both app variants, replacing the b
| File | Change |
|------|--------|
| `android_audio.rs` | Added `start_bluetooth_sco()`, `stop_bluetooth_sco()`, `is_bluetooth_sco_on()`, `is_bluetooth_available()` |
| `lib.rs` | Added `set_bluetooth_sco`, `is_bluetooth_available`, `get_audio_route` Tauri commands |
| `android_audio.rs` | `setCommunicationDevice()` (API 31+) with `startBluetoothSco()` fallback; `set_audio_mode_communication/normal()` for call lifecycle |
| `lib.rs` | `set_bluetooth_sco`, `is_bluetooth_available`, `get_audio_route` Tauri commands; SCO polling + 500ms route delay |
| `wzp_native.rs` | Added `audio_start_bt()` for BT-mode Oboe (skips 48kHz + VoiceCommunication preset) |
| `oboe_bridge.cpp` | `bt_active` flag: capture skips sample rate + input preset; playout uses `Usage::Media`; both use `Shared` mode + `SampleRateConversionQuality::Best` |
| `engine.rs` | `set_audio_mode_communication()` before `audio_start()`; `set_audio_mode_normal()` after `audio_stop()` |
| `MainActivity.kt` | Removed `MODE_IN_COMMUNICATION` from app launch — deferred to call start |
| `main.ts` | Replaced `speakerphoneOn` toggle with `currentAudioRoute` cycling logic |
| `style.css` | Added `.bt-on` CSS class (blue-400 highlight) |
## Audio Route Lifecycle
1. **Call starts** → route defaults to Earpiece
2. **User taps route button** → cycles to next available route
3. **Route changes** → AudioManager JNI call + Oboe stream restart (~60-400ms)
4. **BT device disconnects mid-call**`AudioDeviceCallback.onAudioDevicesRemoved` fires → auto-fallback to Earpiece/Speaker
5. **Call ends** → route reset to Earpiece, BT SCO stopped
1. **App launch**`MODE_NORMAL` (other apps' audio unaffected — BT A2DP music keeps playing)
2. **Call starts**`MODE_IN_COMMUNICATION` set via JNI, Oboe opens with earpiece routing
3. **User taps route button** → cycles to next available route
4. **Route changes**`setCommunicationDevice()` (API 31+) + Oboe restart in BT mode or normal mode
5. **BT device disconnects mid-call**`AudioDeviceCallback.onAudioDevicesRemoved` fires → auto-fallback to Earpiece/Speaker
6. **Call ends** → route reset, `MODE_NORMAL` restored
## Route Cycling Logic
@@ -83,8 +88,10 @@ If BT not available:
## Known Limitations
- **SCO only** — no A2DP (stereo music profile). SCO is correct for VoIP (bidirectional mono).
- **Deprecated APIs** — `startBluetoothSco()`, `isBluetoothScoOn` are deprecated in API 31+ but still functional. Modern replacement `setCommunicationDevice()` requires API 31 and more complex device enumeration. Since minSdk is 26, deprecated path is correct.
- **No auto-switch on BT connect** — when a BT device connects mid-call, `onRouteChanged` fires but we don't auto-switch. User must tap the button.
- **API 31+ required for modern path** — `setCommunicationDevice()` is the primary BT routing API. Fallback to deprecated `startBluetoothSco()` on API < 31 (untested).
- **BT SCO capture at 8/16kHz** — Oboe resamples to 48kHz via `SampleRateConversionQuality::Best`. Quality is inherently limited by the SCO codec (CVSD at 8kHz or mSBC at 16kHz).
- **No auto-switch on BT connect** — when a BT device connects mid-call, user must tap the route button.
- **500ms route switch delay** — after `setCommunicationDevice()` returns, the audio policy needs time to apply the bt-sco route. We wait 500ms before restarting Oboe.
## Testing

View File

@@ -191,3 +191,26 @@ Run with `wzp-bench --all`. Representative results (Apple M-series, single core)
- **Hetzner VPS**: Build script (`scripts/build-linux.sh`) tested for provisioning, building, and downloading Linux binaries
- **CI**: Gitea workflow defined for amd64/arm64/armv7 builds
- **Production**: Not yet deployed to production networks
## Recent Changes (2026-04-12)
### Bluetooth Audio Routing
- 3-way route cycling: Earpiece → Speaker → Bluetooth SCO
- `setCommunicationDevice()` API 31+ with `startBluetoothSco()` fallback
- BT-mode Oboe: capture skips 48kHz + VoiceCommunication, Oboe resamples 8/16kHz ↔ 48kHz
- `MODE_IN_COMMUNICATION` deferred to call start (was at app launch — hijacked system audio)
### Network Change Detection
- `NetworkMonitor.kt` wraps `ConnectivityManager.NetworkCallback`
- WiFi/cellular classification via bandwidth heuristics (no READ_PHONE_STATE needed)
- Feeds `AdaptiveQualityController::signal_network_change()` via JNI → AtomicU8 → recv task
### Hangup Signal Fix
- `SignalMessage::Hangup` now carries optional `call_id`
- Relay only ends the named call (not all calls for the user)
- Fixes race: hangup for call 1 no longer kills newly-placed call 2
### Per-Architecture APK Builds
- `build-tauri-android.sh --arch arm64|armv7|all`
- Separate per-arch APKs (~25MB each vs ~50MB universal)
- Release APKs signed with `wzp-release.jks` via `apksigner`