fix(android): adapt fold layout and diagnostics

This commit is contained in:
Edison Jwa
2026-05-20 14:52:34 +09:00
parent 30ff955439
commit 3ae8e1ab77
3 changed files with 80 additions and 71 deletions
+18 -29
View File
@@ -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<rust.BridgeChannel> 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 ||
+7 -22
View File
@@ -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<AudioSessionId> {
// 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`,
+55 -20
View File
@@ -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::<String>().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<BridgeSnapshot, BridgeError> {
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<Vec<BridgeBookmark>, 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<i64, BridgeError> {
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<BridgeAudioStats, BridgeError> {
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,