From 15c237ceea7934255a9ec29e2a1ede29c4ae355f Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Sun, 12 Apr 2026 17:29:59 +0400 Subject: [PATCH] fix(audio): defer MODE_IN_COMMUNICATION to call start, restore on end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: MainActivity set MODE_IN_COMMUNICATION at app launch, hijacking system audio routing immediately — BT A2DP music dropped to earpiece, and the pre-existing communication mode confused subsequent setCommunicationDevice calls for BT SCO. Fix: MainActivity now only sets volumes. MODE_IN_COMMUNICATION is set via JNI right before Oboe audio_start() in CallEngine, and MODE_NORMAL is restored after audio_stop() when the call ends. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../main/java/com/wzp/desktop/MainActivity.kt | 16 +++++---- desktop/src-tauri/src/android_audio.rs | 34 ++++++++++++++++--- desktop/src-tauri/src/engine.rs | 16 +++++++++ 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/desktop/src-tauri/gen/android/app/src/main/java/com/wzp/desktop/MainActivity.kt b/desktop/src-tauri/gen/android/app/src/main/java/com/wzp/desktop/MainActivity.kt index 1d4bba6..b6a5b9e 100644 --- a/desktop/src-tauri/gen/android/app/src/main/java/com/wzp/desktop/MainActivity.kt +++ b/desktop/src-tauri/gen/android/app/src/main/java/com/wzp/desktop/MainActivity.kt @@ -72,18 +72,22 @@ class MainActivity : TauriActivity() { * STREAM_VOICE_CALL volume is cranked to max since the in-call volume * slider is separate from media volume on most devices. */ + /** + * Pre-flight: only set volumes. Do NOT set MODE_IN_COMMUNICATION here — + * that hijacks the entire audio routing (music stops, BT A2DP drops to + * earpiece) even before a call starts. The Rust side sets the mode via + * JNI when the call engine actually starts, and restores MODE_NORMAL + * when the call ends. + */ private fun configureAudioForCall() { try { val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager - Log.i(TAG, "audio state before: mode=${am.mode} speaker=${am.isSpeakerphoneOn} " + + Log.i(TAG, "audio state: mode=${am.mode} speaker=${am.isSpeakerphoneOn} " + "voiceVol=${am.getStreamVolume(AudioManager.STREAM_VOICE_CALL)}/" + "${am.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL)} " + "musicVol=${am.getStreamVolume(AudioManager.STREAM_MUSIC)}/" + "${am.getStreamMaxVolume(AudioManager.STREAM_MUSIC)}") - am.mode = AudioManager.MODE_IN_COMMUNICATION - am.isSpeakerphoneOn = false // default: handset / earpiece - // Crank both voice-call and music volumes so nothing silent slips // through regardless of which stream actually ends up driving. val maxVoice = am.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL) @@ -91,9 +95,7 @@ class MainActivity : TauriActivity() { val maxMusic = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC) am.setStreamVolume(AudioManager.STREAM_MUSIC, maxMusic, 0) - Log.i(TAG, "audio state after: mode=${am.mode} speaker=${am.isSpeakerphoneOn} " + - "voiceVol=${am.getStreamVolume(AudioManager.STREAM_VOICE_CALL)}/$maxVoice " + - "musicVol=${am.getStreamVolume(AudioManager.STREAM_MUSIC)}/$maxMusic") + Log.i(TAG, "volumes set: voiceVol=$maxVoice musicVol=$maxMusic (mode left at ${am.mode})") } catch (e: Throwable) { Log.e(TAG, "configureAudioForCall failed: ${e.message}", e) } diff --git a/desktop/src-tauri/src/android_audio.rs b/desktop/src-tauri/src/android_audio.rs index 048359b..d7016c4 100644 --- a/desktop/src-tauri/src/android_audio.rs +++ b/desktop/src-tauri/src/android_audio.rs @@ -57,11 +57,37 @@ fn audio_manager<'local>( Ok(am) } +/// Set `AudioManager.MODE_IN_COMMUNICATION`. Call when a VoIP call starts. +/// This tells the audio policy to route through the communication device +/// path (earpiece/BT SCO) instead of the media path (speaker/BT A2DP). +pub fn set_audio_mode_communication() -> Result<(), String> { + let (vm, activity) = jvm_and_activity()?; + let mut env = vm + .attach_current_thread() + .map_err(|e| format!("attach_current_thread: {e}"))?; + let am = audio_manager(&mut env, &activity)?; + // MODE_IN_COMMUNICATION = 3 + env.call_method(&am, "setMode", "(I)V", &[JValue::Int(3)]) + .map_err(|e| format!("setMode(MODE_IN_COMMUNICATION): {e}"))?; + tracing::info!("AudioManager: mode set to MODE_IN_COMMUNICATION"); + Ok(()) +} + +/// Restore `AudioManager.MODE_NORMAL`. Call when a VoIP call ends. +pub fn set_audio_mode_normal() -> Result<(), String> { + let (vm, activity) = jvm_and_activity()?; + let mut env = vm + .attach_current_thread() + .map_err(|e| format!("attach_current_thread: {e}"))?; + let am = audio_manager(&mut env, &activity)?; + // MODE_NORMAL = 0 + env.call_method(&am, "setMode", "(I)V", &[JValue::Int(0)]) + .map_err(|e| format!("setMode(MODE_NORMAL): {e}"))?; + tracing::info!("AudioManager: mode set to MODE_NORMAL"); + Ok(()) +} + /// Switch between loud speaker (`true`) and earpiece/handset (`false`). -/// -/// Calls `AudioManager.setSpeakerphoneOn(on)` on the JVM. Requires that -/// the audio mode is already `MODE_IN_COMMUNICATION` — MainActivity.kt -/// sets this at startup, so by the time a call is up this is always true. pub fn set_speakerphone(on: bool) -> Result<(), String> { let (vm, activity) = jvm_and_activity()?; let mut env = vm diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index 4cd18fe..3a0b378 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -436,6 +436,16 @@ impl CallEngine { // hitting a "device busy" on some HALs. tokio::time::sleep(std::time::Duration::from_millis(50)).await; + // Set MODE_IN_COMMUNICATION right before audio starts — NOT at + // app launch. Setting it early hijacks system audio routing + // (music drops from BT A2DP to earpiece, etc.). + #[cfg(target_os = "android")] + { + if let Err(e) = crate::android_audio::set_audio_mode_communication() { + warn!("set_audio_mode_communication failed: {e}"); + } + } + let t_pre_audio = call_t0.elapsed().as_millis(); if let Err(code) = crate::wzp_native::audio_start() { return Err(anyhow::anyhow!("wzp_native_audio_start failed: code {code}")); @@ -1490,6 +1500,12 @@ impl CallEngine { #[cfg(target_os = "android")] { crate::wzp_native::audio_stop(); + // Restore MODE_NORMAL so other apps' audio (music, etc.) + // routes normally again. Without this, the system stays in + // communication mode and BT A2DP music goes to earpiece. + if let Err(e) = crate::android_audio::set_audio_mode_normal() { + tracing::warn!("set_audio_mode_normal failed: {e}"); + } } } }