diff --git a/apps/chanora_flutter/lib/main.dart b/apps/chanora_flutter/lib/main.dart index 1953eae..04814f5 100644 --- a/apps/chanora_flutter/lib/main.dart +++ b/apps/chanora_flutter/lib/main.dart @@ -1234,7 +1234,7 @@ class _BetaHomeState extends State<_BetaHome> { final headerActions = [ if (_phase == _Phase.connected && _inChannel && - MediaQuery.of(context).size.width < 840.0) ...[ + MediaQuery.of(context).size.width < 600.0) ...[ IconButton( tooltip: l10n.voiceHardMuteLabel, icon: Icon(_hardMute ? Icons.mic_off : Icons.mic), @@ -1268,7 +1268,7 @@ class _BetaHomeState extends State<_BetaHome> { final bodyContent = LayoutBuilder( builder: (ctx, bodyConstraints) { - const wideBreakpoint = 840.0; + const wideBreakpoint = 600.0; final isWideSnapshot = bodyConstraints.maxWidth >= wideBreakpoint && _phase == _Phase.connected && @@ -1390,9 +1390,7 @@ class _BetaHomeState extends State<_BetaHome> { pendingVoiceChannelId: _pendingVoiceChannelId, hasJoinPending: _pendingVoiceChannelId != null, canJoinVoiceChannel: _canJoinVoiceChannel, - canLeaveVoiceChannel: _canLeaveVoiceChannel, onJoinChannel: _onJoinChannel, - onLeaveVoice: _onLeaveVoice, ); const voiceBarWidthWide = 320.0; if (constraints.maxWidth >= wideBreakpoint) { @@ -2047,9 +2045,7 @@ class _SnapshotView extends StatelessWidget { required this.pendingVoiceChannelId, required this.hasJoinPending, required this.canJoinVoiceChannel, - required this.canLeaveVoiceChannel, required this.onJoinChannel, - required this.onLeaveVoice, }); final rust.BridgeSnapshot snapshot; @@ -2057,9 +2053,7 @@ class _SnapshotView extends StatelessWidget { final BigInt? pendingVoiceChannelId; final bool hasJoinPending; final bool canJoinVoiceChannel; - final bool canLeaveVoiceChannel; final ValueChanged onJoinChannel; - final VoidCallback onLeaveVoice; @override Widget build(BuildContext context) { @@ -2136,27 +2130,22 @@ class _SnapshotView extends StatelessWidget { ), title: Text(ch.name), subtitle: Text('id=${ch.id} parent=${ch.parent}'), - trailing: IconButton( - icon: Icon( - ch.id == currentVoiceChannelId - ? Icons.logout - : ch.id == pendingVoiceChannelId - ? Icons.hourglass_top - : Icons.login, - ), - tooltip: ch.id == currentVoiceChannelId - ? l10n.leaveChannelAction - : ch.id == pendingVoiceChannelId - ? l10n.statusConnecting - : l10n.joinChannelAction, - onPressed: hasJoinPending - ? null - : ch.id == pendingVoiceChannelId - ? null - : ch.id == currentVoiceChannelId - ? (canLeaveVoiceChannel ? onLeaveVoice : null) - : () => onJoinChannel(ch), - ), + trailing: ch.id == currentVoiceChannelId + ? null + : IconButton( + icon: Icon( + ch.id == pendingVoiceChannelId + ? Icons.hourglass_top + : Icons.login, + ), + tooltip: ch.id == pendingVoiceChannelId + ? l10n.statusConnecting + : l10n.joinChannelAction, + onPressed: + hasJoinPending || ch.id == pendingVoiceChannelId + ? null + : () => onJoinChannel(ch), + ), selected: ch.id == currentVoiceChannelId, onTap: hasJoinPending || diff --git a/crates/chanora_audio/src/android_voice_unit.rs b/crates/chanora_audio/src/android_voice_unit.rs index b0c8d7c..e679947 100644 --- a/crates/chanora_audio/src/android_voice_unit.rs +++ b/crates/chanora_audio/src/android_voice_unit.rs @@ -440,14 +440,13 @@ impl AndroidVoiceUnit { let input_share = share_from_oboe(input_stream.get_sharing_mode()); let input_sample_rate = input_stream.get_sample_rate(); let input_frames_per_burst = input_stream.get_frames_per_burst(); - let session_id = match input_stream.get_session_id() { - SessionId::None => None, - // `SessionId::Allocate` is the request value; once a - // stream is open, oboe returns the concrete allocated - // id via `SessionId::Id(i32)` in some versions or via - // a getter — defensively match. - other => session_id_value(other), - }; + let session_id = None; + warn!( + target: "chanora_audio", + "android: oboe-rs get_session_id is skipped because oboe 0.6.1 \ + panics on some Android allocated-session values; hardware \ + effects are disabled for this stream" + ); // --- Open output stream (SDD-112) -------------------------- let output_builder = AudioStreamBuilder::default() @@ -829,20 +828,6 @@ fn share_from_oboe(s: SharingMode) -> AchievedSharingMode { } } -fn session_id_value(sid: SessionId) -> Option { - // SessionId is a Rust enum from oboe-rs. `Allocate` is the - // request token; once allocated the platform returns a positive - // integer carried in the enum's tuple variant. Match explicitly - // — `mem::transmute` would be UB on a non-`#[repr(i32)]` enum, - // even if it happens to lay out correctly today. - match sid { - SessionId::None => None, - // `SessionId::Allocate` is a request marker; treat it as - // "id not yet known" rather than fabricating one. - SessionId::Allocate => None, - } -} - // --- SDD-113 hardware-effect binding ----------------------------- // // We attach `AcousticEchoCanceler`, `NoiseSuppressor`, diff --git a/crates/chanora_bridge/src/api.rs b/crates/chanora_bridge/src/api.rs index 113e30e..e802532 100644 --- a/crates/chanora_bridge/src/api.rs +++ b/crates/chanora_bridge/src/api.rs @@ -32,6 +32,40 @@ fn runtime() -> &'static Runtime { }) } +fn task_join_error(task: &'static str, error: tokio::task::JoinError) -> BridgeError { + warn!( + target: "chanora_bridge", + task, + error = %error, + "runtime task failed; surfacing in diagnostics" + ); + BridgeError::Unmapped(format!("join: {error}")) +} + +fn install_panic_diagnostic_hook() { + static INSTALLED: OnceLock<()> = OnceLock::new(); + let _ = INSTALLED.get_or_init(|| { + std::panic::set_hook(Box::new(|panic_info| { + let message = panic_info + .payload() + .downcast_ref::<&'static str>() + .map(|s| (*s).to_string()) + .or_else(|| panic_info.payload().downcast_ref::().cloned()) + .unwrap_or_else(|| "non-string panic payload".to_string()); + let location = panic_info + .location() + .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column())) + .unwrap_or_else(|| "unknown".to_string()); + warn!( + target: "chanora_bridge", + panic_message = %message, + panic_location = %location, + "panic captured for diagnostic export" + ); + })); + }); +} + /// Process-wide session handle. One instance per process is enough /// for Alpha (DEC-006 single-connection invariant); the Mutex inside /// `ChanoraSession` enforces the connection-count invariant. @@ -177,6 +211,7 @@ pub fn bridge_init() { } } + install_panic_diagnostic_hook(); info!(target: "chanora_bridge", "bridge initialised"); if let Some(p) = log_file_path() { info!(target: "chanora_bridge", path = %p.display(), "log file path"); @@ -415,7 +450,7 @@ pub async fn connect( let snap = runtime() .spawn(async move { session().connect(cfg).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("connect", e))??; Ok(snap.into()) } @@ -424,7 +459,7 @@ pub async fn snapshot() -> Result { let snap = runtime() .spawn(async { session().snapshot().await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("snapshot", e))??; Ok(snap.into()) } @@ -433,7 +468,7 @@ pub async fn disconnect() -> Result<(), BridgeError> { runtime() .spawn(async { session().disconnect().await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("disconnect", e))??; Ok(()) } @@ -488,7 +523,7 @@ pub async fn set_ptt(active: bool) -> Result<(), BridgeError> { runtime() .spawn(async move { session().set_ptt(active).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("set_ptt", e))??; Ok(()) } @@ -545,7 +580,7 @@ pub async fn voice_join(channel_id: u64, password: String) -> Result<(), BridgeE runtime() .spawn(async move { session().voice_join(channel_id, pw).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("voice_join", e))??; Ok(()) } @@ -555,7 +590,7 @@ pub async fn voice_leave() -> Result<(), BridgeError> { runtime() .spawn(async { session().voice_leave().await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("voice_leave", e))??; Ok(()) } @@ -564,7 +599,7 @@ pub async fn set_transmit_mode(mode: BridgeTransmitMode) -> Result<(), BridgeErr runtime() .spawn(async move { session().set_transmit_mode(mode.into()).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("set_transmit_mode", e))??; Ok(()) } @@ -584,7 +619,7 @@ pub async fn set_release_tail_ms(ms: u32) -> Result<(), BridgeError> { runtime() .spawn(async move { session().set_release_tail_ms(ms).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("set_release_tail_ms", e))??; Ok(()) } @@ -602,7 +637,7 @@ pub async fn set_hard_mute(muted: bool) -> Result<(), BridgeError> { runtime() .spawn(async move { session().set_hard_mute(muted).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("set_hard_mute", e))??; Ok(()) } @@ -644,7 +679,7 @@ pub async fn set_ptt_binding( runtime() .spawn(async move { session().set_ptt_binding(binding).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("set_ptt_binding", e))??; Ok(()) } @@ -683,7 +718,7 @@ pub async fn move_to_channel(channel_id: u64, password: String) -> Result<(), Br runtime() .spawn(async move { session().move_to_channel(channel_id, pw).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("move_to_channel", e))??; Ok(()) } @@ -694,7 +729,7 @@ pub async fn set_input_muted(muted: bool) -> Result<(), BridgeError> { runtime() .spawn(async move { session().set_self_muted(Some(muted), None).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("set_input_muted", e))??; Ok(()) } @@ -706,7 +741,7 @@ pub async fn set_output_muted(muted: bool) -> Result<(), BridgeError> { runtime() .spawn(async move { session().set_self_muted(None, Some(muted)).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("set_output_muted", e))??; Ok(()) } @@ -717,7 +752,7 @@ pub async fn set_output_gain(gain: f32) -> Result<(), BridgeError> { runtime() .spawn(async move { session().set_output_gain(gain).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("set_output_gain", e))??; Ok(()) } @@ -790,7 +825,7 @@ pub async fn init_storage(dir: String) -> Result<(), BridgeError> { runtime() .spawn(async move { session().init_storage(&dir).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("init_storage", e))??; Ok(()) } @@ -843,7 +878,7 @@ pub async fn list_bookmarks() -> Result, BridgeError> { let v = runtime() .spawn(async { session().list_bookmarks().await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("list_bookmarks", e))??; Ok(v.into_iter().map(Into::into).collect()) } @@ -854,7 +889,7 @@ pub async fn add_bookmark(b: BridgeBookmark) -> Result { let id = runtime() .spawn(async move { session().add_bookmark(core_b).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("add_bookmark", e))??; Ok(id) } @@ -864,7 +899,7 @@ pub async fn update_bookmark(b: BridgeBookmark) -> Result<(), BridgeError> { runtime() .spawn(async move { session().update_bookmark(core_b).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("update_bookmark", e))??; Ok(()) } @@ -873,7 +908,7 @@ pub async fn delete_bookmark(id: i64) -> Result<(), BridgeError> { runtime() .spawn(async move { session().delete_bookmark(id).await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("delete_bookmark", e))??; Ok(()) } @@ -1273,7 +1308,7 @@ pub async fn audio_stats() -> Result { let (s, r, p) = runtime() .spawn(async { session().audio_stats().await }) .await - .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??; + .map_err(|e| task_join_error("audio_stats", e))??; Ok(BridgeAudioStats { frames_sent: s, frames_received: r,