From f1f81a3d7e4b6813e092474211b22732dcf129e6 Mon Sep 17 00:00:00 2001 From: EdisonJwa Date: Sat, 16 May 2026 17:16:17 +0800 Subject: [PATCH] fix(core,bridge,ios): voice_join survives audio-engine failure; iOS log file User report from iPhone: 'mute / continuous / PTT buttons missing' with NO error popup. Root cause: voice_join's audio-engine startup was failing silently, and the failure propagated out as a hard error \u2014 which means SessionEvent::VoiceState(true) was never sent to Dart even though the server-side channel move had already succeeded. Dart's _inChannel stayed false; every control gated on _inChannel disappeared while the channel tree continued to show the user as joined. This commit makes voice_join lenient on audio-engine failures so the UI state matches the server-side reality, and also gives iOS a writable log file so diagnostics from device builds are recoverable for the first time. core/chanora_core/src/lib.rs::voice_join * ensure_audio_running's error is now logged + emitted as SessionEvent::AudioStopped, but does NOT abort voice_join. The server move at step 1 already succeeded; failing the Dart-visible promise here would leave the UI in a phantom 'in-channel visually but no controls' state. After this commit: - mic / headset / settings appear in the AppBar - PTT button appears at the bottom - status chip shows live audio state ('Mic on/off') - if audio actually failed (mic permission denied, no input device, CoreAudio rejecting stream config) the user can retry by switching modes / channels; BridgeEvent::AudioStopped wires _audioStarted=false in Dart so audio-stats poll is honest about the engine state. crates/chanora_bridge/src/api.rs::log_file_path * iOS now writes the log to /home/milkice/Documents/chanora.log (Documents is the standard user-visible iOS sandbox dir). * Android remains None pending the bridge JNI init wiring a writable path (P1 follow-up). apps/chanora_flutter/ios/Runner/Info.plist * Adds UIFileSharingEnabled + LSSupportsOpeningDocumentsInPlace so the Documents directory shows up under 'On My iPhone \u2192 Chanora' in the Files.app. The user can now copy chanora.log out for support without needing Xcode \u2192 Devices and Simulators \u2192 Download Container. Workspace tests: 78/0/1 unchanged. flutter build ios --release --no-codesign: 22.7 s clean (Runner.app 29.9 MB). --- apps/chanora_flutter/ios/Runner/Info.plist | 9 ++++++ core/chanora_core/src/lib.rs | 32 ++++++++++++++++++++-- crates/chanora_bridge/src/api.rs | 25 ++++++++++++++++- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/apps/chanora_flutter/ios/Runner/Info.plist b/apps/chanora_flutter/ios/Runner/Info.plist index b1ba0bb..590b676 100644 --- a/apps/chanora_flutter/ios/Runner/Info.plist +++ b/apps/chanora_flutter/ios/Runner/Info.plist @@ -72,5 +72,14 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + + UIFileSharingEnabled + + LSSupportsOpeningDocumentsInPlace + diff --git a/core/chanora_core/src/lib.rs b/core/chanora_core/src/lib.rs index f5d7858..52e2523 100644 --- a/core/chanora_core/src/lib.rs +++ b/core/chanora_core/src/lib.rs @@ -918,8 +918,36 @@ impl ChanoraSession { self.emit_voice_state(false).await; return Err(e); } - // 2. Bring the audio engine up. - self.ensure_audio_running().await?; + // 2. Bring the audio engine up. Tolerate failure: the + // server-side channel move has ALREADY succeeded (step + // 1), so the user is in the channel from every other + // peer's perspective. Failing voice_join hard here would + // leave the UI in a phantom "you're in the channel + // visually but no controls" state because the calling + // Dart code wouldn't receive the VoiceState(true) event + // that gates the AppBar mute icons + the on-screen PTT + // button. Honest behaviour: surface the audio error + // once on the event channel (via the AudioStopped event + // consumers already handle), then continue so the UI + // matches reality — user is in the channel, but mic / + // speakers may be silent until they resolve the audio + // error (e.g. grant mic permission, plug in a working + // device). + if let Err(audio_err) = self.ensure_audio_running().await { + warn!( + target: "chanora_core", + error = %audio_err, + channel_id, + "voice_join: server move succeeded but audio engine \ + failed to start; continuing with no-audio in-channel \ + state so the UI matches the server-side state" + ); + // Tell subscribers the audio engine is not running so + // any audio-stats poll / level meter renders correctly. + // The voice_join itself still resolves Ok below so the + // UI gains the channel + mute + PTT controls. + let _ = self.events_tx.send(SessionEvent::AudioStopped); + } // 3. Belt-and-braces: if the server replied Ok but never // actually moved us (legacy server, command processed // but rolled back later, etc.) the snapshot poll catches diff --git a/crates/chanora_bridge/src/api.rs b/crates/chanora_bridge/src/api.rs index 7bf429d..cc4ebdf 100644 --- a/crates/chanora_bridge/src/api.rs +++ b/crates/chanora_bridge/src/api.rs @@ -180,8 +180,31 @@ fn log_file_path() -> Option { .join("chanora.log"), ) } - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(target_os = "ios")] { + // iOS sandbox: write the log to the app's Documents + // directory so it persists across launches and can be + // pulled via Xcode -> Devices and Simulators -> Download + // Container, OR via Files.app on the device (the app + // appears under "On My iPhone" once we declare + // UIFileSharingEnabled + LSSupportsOpeningDocumentsInPlace + // in Info.plist — done in a follow-up). + // + // HOME on iOS resolves to the app sandbox root; Documents + // is the standard user-visible subdirectory. + let home = std::env::var_os("HOME")?; + Some( + std::path::PathBuf::from(home) + .join("Documents") + .join("chanora.log"), + ) + } + #[cfg(target_os = "android")] + { + // Android log file location is set up via the bridge's + // Java-side init that writes the chosen path into an env + // var (not currently wired; P1 follow-up). For now we + // return None and rely on logcat. None } }