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).
This commit is contained in:
EdisonJwa
2026-05-16 17:16:17 +08:00
parent 3b08ea9d32
commit f1f81a3d7e
3 changed files with 63 additions and 3 deletions
@@ -72,5 +72,14 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<!-- Make Chanora's Documents folder visible to the Files.app and
accessible via iTunes / Finder file-sharing. We write
diagnostic logs (chanora.log) into Documents/ so users can
export them for support. Both keys are required for the
"On My iPhone -> Chanora" listing to appear in Files.app. -->
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
</dict> </dict>
</plist> </plist>
+30 -2
View File
@@ -918,8 +918,36 @@ impl ChanoraSession {
self.emit_voice_state(false).await; self.emit_voice_state(false).await;
return Err(e); return Err(e);
} }
// 2. Bring the audio engine up. // 2. Bring the audio engine up. Tolerate failure: the
self.ensure_audio_running().await?; // 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 // 3. Belt-and-braces: if the server replied Ok but never
// actually moved us (legacy server, command processed // actually moved us (legacy server, command processed
// but rolled back later, etc.) the snapshot poll catches // but rolled back later, etc.) the snapshot poll catches
+24 -1
View File
@@ -180,8 +180,31 @@ fn log_file_path() -> Option<std::path::PathBuf> {
.join("chanora.log"), .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 None
} }
} }