Files
wz-phone/desktop/src-tauri/src/android_audio.rs
Siavash Sameni 4f2ad65418
Some checks failed
Mirror to GitHub / mirror (push) Failing after 37s
Build Release Binaries / build-amd64 (push) Failing after 4m6s
fix(android_audio): add explicit pointer types for .cast() — was rejected by rustc E0282 on android target
2026-04-09 22:02:48 +04:00

99 lines
3.7 KiB
Rust

//! Runtime bridge to Android's `AudioManager` for in-call audio routing.
//!
//! We own a quinn+Oboe VoIP pipeline entirely from Rust, but routing the
//! playout stream between earpiece / loudspeaker / Bluetooth headset has to
//! happen at the JVM level because those toggles are AudioManager-only.
//! This module uses the global JavaVM handle that `ndk_context` exposes
//! (populated by Tauri's mobile runtime) + the `jni` crate to reach into
//! the Android framework without needing a Tauri plugin.
//!
//! All callers must be inside an Android target (`#[cfg(target_os = "android")]`).
#![cfg(target_os = "android")]
use jni::objects::{JObject, JString, JValue};
use jni::JavaVM;
/// Grab the JavaVM + current Activity from the ndk_context that Tauri's
/// mobile runtime sets up at process startup.
fn jvm_and_activity() -> Result<(JavaVM, JObject<'static>), String> {
let ctx = ndk_context::android_context();
let vm_ptr = ctx.vm() as *mut jni::sys::JavaVM;
if vm_ptr.is_null() {
return Err("ndk_context: JavaVM pointer is null".into());
}
let vm = unsafe { JavaVM::from_raw(vm_ptr) }
.map_err(|e| format!("JavaVM::from_raw: {e}"))?;
let activity_ptr = ctx.context() as jni::sys::jobject;
if activity_ptr.is_null() {
return Err("ndk_context: activity pointer is null".into());
}
// SAFETY: ndk_context guarantees the pointer lives for the process
// lifetime; we wrap it as a JObject<'static> for convenience.
let activity: JObject<'static> = unsafe { JObject::from_raw(activity_ptr) };
Ok((vm, activity))
}
/// Get Android's `AudioManager` via `activity.getSystemService("audio")`.
fn audio_manager<'local>(
env: &mut jni::AttachGuard<'local>,
activity: &JObject<'local>,
) -> Result<JObject<'local>, String> {
let svc_name: JString<'local> = env
.new_string("audio")
.map_err(|e| format!("new_string(audio): {e}"))?;
let am = env
.call_method(
activity,
"getSystemService",
"(Ljava/lang/String;)Ljava/lang/Object;",
&[JValue::Object(&svc_name)],
)
.and_then(|v| v.l())
.map_err(|e| format!("getSystemService(audio): {e}"))?;
if am.is_null() {
return Err("getSystemService returned null".into());
}
Ok(am)
}
/// 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
.attach_current_thread()
.map_err(|e| format!("attach_current_thread: {e}"))?;
let am = audio_manager(&mut env, &activity)?;
env.call_method(
&am,
"setSpeakerphoneOn",
"(Z)V",
&[JValue::Bool(if on { 1 } else { 0 })],
)
.map_err(|e| format!("setSpeakerphoneOn({on}): {e}"))?;
tracing::info!(on, "AudioManager.setSpeakerphoneOn");
Ok(())
}
/// Query the current speakerphone state. Returns true if routing is on the
/// loud speaker, false if on earpiece / BT headset / wired headset.
pub fn is_speakerphone_on() -> Result<bool, 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)?;
let on = env
.call_method(&am, "isSpeakerphoneOn", "()Z", &[])
.and_then(|v| v.z())
.map_err(|e| format!("isSpeakerphoneOn: {e}"))?;
Ok(on)
}