feat(android): Bluetooth audio routing + network change detection + per-arch APK builds
Bluetooth: wire existing AudioRouteManager SCO support through both app variants. Replace binary speaker toggle with 3-way route cycling (Earpiece → Speaker → Bluetooth). Tauri side adds JNI bridge functions (start/stop/query SCO, device availability) and Oboe stream restart. Network awareness: integrate Android ConnectivityManager to detect WiFi/cellular transitions and feed them to AdaptiveQualityController via lock-free AtomicU8 signaling. Enables proactive quality downgrade and FEC boost on network handoffs. Build: add --arch flag to build-tauri-android.sh supporting arm64, armv7, or all (separate per-arch APKs for smaller tester binaries). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -96,3 +96,134 @@ pub fn is_speakerphone_on() -> Result<bool, String> {
|
||||
.map_err(|e| format!("isSpeakerphoneOn: {e}"))?;
|
||||
Ok(on)
|
||||
}
|
||||
|
||||
// ─── Bluetooth SCO routing ──────────────────────────────────────────────────
|
||||
|
||||
/// Start Bluetooth SCO (Synchronous Connection Oriented) audio routing.
|
||||
///
|
||||
/// Turns off the loudspeaker, then opens the SCO link so both capture and
|
||||
/// playout move to the connected Bluetooth headset. Requires that a SCO-
|
||||
/// capable device is paired and connected (check [`is_bluetooth_available`]
|
||||
/// first). The caller must restart Oboe streams after this call.
|
||||
#[allow(deprecated)]
|
||||
pub fn start_bluetooth_sco() -> 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)?;
|
||||
|
||||
// Ensure speaker is off — mutually exclusive with SCO.
|
||||
env.call_method(
|
||||
&am,
|
||||
"setSpeakerphoneOn",
|
||||
"(Z)V",
|
||||
&[JValue::Bool(0)],
|
||||
)
|
||||
.map_err(|e| format!("setSpeakerphoneOn(false): {e}"))?;
|
||||
|
||||
env.call_method(&am, "startBluetoothSco", "()V", &[])
|
||||
.map_err(|e| format!("startBluetoothSco: {e}"))?;
|
||||
|
||||
env.call_method(
|
||||
&am,
|
||||
"setBluetoothScoOn",
|
||||
"(Z)V",
|
||||
&[JValue::Bool(1)],
|
||||
)
|
||||
.map_err(|e| format!("setBluetoothScoOn(true): {e}"))?;
|
||||
|
||||
tracing::info!("AudioManager: Bluetooth SCO started");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop Bluetooth SCO audio routing, returning audio to the earpiece.
|
||||
///
|
||||
/// Safe to call even if SCO is not currently active (no-ops in that case).
|
||||
/// The caller must restart Oboe streams after this call.
|
||||
#[allow(deprecated)]
|
||||
pub fn stop_bluetooth_sco() -> 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)?;
|
||||
|
||||
let is_on = env
|
||||
.call_method(&am, "isBluetoothScoOn", "()Z", &[])
|
||||
.and_then(|v| v.z())
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_on {
|
||||
env.call_method(
|
||||
&am,
|
||||
"setBluetoothScoOn",
|
||||
"(Z)V",
|
||||
&[JValue::Bool(0)],
|
||||
)
|
||||
.map_err(|e| format!("setBluetoothScoOn(false): {e}"))?;
|
||||
|
||||
env.call_method(&am, "stopBluetoothSco", "()V", &[])
|
||||
.map_err(|e| format!("stopBluetoothSco: {e}"))?;
|
||||
}
|
||||
|
||||
tracing::info!(was_on = is_on, "AudioManager: Bluetooth SCO stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Query whether Bluetooth SCO audio is currently active.
|
||||
#[allow(deprecated)]
|
||||
pub fn is_bluetooth_sco_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)?;
|
||||
|
||||
env.call_method(&am, "isBluetoothScoOn", "()Z", &[])
|
||||
.and_then(|v| v.z())
|
||||
.map_err(|e| format!("isBluetoothScoOn: {e}"))
|
||||
}
|
||||
|
||||
/// Check whether a Bluetooth SCO-capable device is currently connected.
|
||||
///
|
||||
/// Iterates `AudioManager.getDevices(GET_DEVICES_OUTPUTS)` and looks for
|
||||
/// `TYPE_BLUETOOTH_SCO` (7).
|
||||
pub fn is_bluetooth_available() -> 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)?;
|
||||
|
||||
// AudioManager.GET_DEVICES_OUTPUTS = 2
|
||||
let devices = env
|
||||
.call_method(
|
||||
&am,
|
||||
"getDevices",
|
||||
"(I)[Landroid/media/AudioDeviceInfo;",
|
||||
&[JValue::Int(2)],
|
||||
)
|
||||
.and_then(|v| v.l())
|
||||
.map_err(|e| format!("getDevices(OUTPUTS): {e}"))?;
|
||||
|
||||
let arr = jni::objects::JObjectArray::from(devices);
|
||||
let len = env
|
||||
.get_array_length(&arr)
|
||||
.map_err(|e| format!("get_array_length: {e}"))?;
|
||||
|
||||
for i in 0..len {
|
||||
let device = env
|
||||
.get_object_array_element(&arr, i)
|
||||
.map_err(|e| format!("get_object_array_element({i}): {e}"))?;
|
||||
let device_type = env
|
||||
.call_method(&device, "getType", "()I", &[])
|
||||
.and_then(|v| v.i())
|
||||
.unwrap_or(0);
|
||||
// TYPE_BLUETOOTH_SCO = 7
|
||||
if device_type == 7 {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
@@ -775,6 +775,71 @@ async fn is_speakerphone_on() -> Result<bool, String> {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Bluetooth SCO routing (Android-specific, no-op on desktop) ─────────────
|
||||
|
||||
/// Enable or disable Bluetooth SCO audio routing. Like speakerphone toggling,
|
||||
/// this requires an Oboe stream restart so AAudio picks up the new route.
|
||||
#[tauri::command]
|
||||
#[allow(unused_variables)]
|
||||
async fn set_bluetooth_sco(on: bool) -> Result<(), String> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
if on {
|
||||
android_audio::start_bluetooth_sco()?;
|
||||
} else {
|
||||
android_audio::stop_bluetooth_sco()?;
|
||||
}
|
||||
if wzp_native::is_loaded() && wzp_native::audio_is_running() {
|
||||
tracing::info!(on, "set_bluetooth_sco: restarting Oboe for route change");
|
||||
tokio::task::spawn_blocking(|| {
|
||||
wzp_native::audio_stop();
|
||||
wzp_native::audio_start()
|
||||
.map_err(|code| format!("audio_start after BT toggle: code {code}"))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("spawn_blocking join: {e}"))??;
|
||||
tracing::info!("set_bluetooth_sco: Oboe restarted");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether a Bluetooth SCO device is currently connected and available.
|
||||
#[tauri::command]
|
||||
async fn is_bluetooth_available() -> Result<bool, String> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
android_audio::is_bluetooth_available()
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the current audio route as a string: "bluetooth", "speaker", or "earpiece".
|
||||
#[tauri::command]
|
||||
async fn get_audio_route() -> Result<String, String> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
if android_audio::is_bluetooth_sco_on()? {
|
||||
return Ok("bluetooth".into());
|
||||
}
|
||||
if android_audio::is_speakerphone_on()? {
|
||||
return Ok("speaker".into());
|
||||
}
|
||||
Ok("earpiece".into())
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
Ok("earpiece".into())
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Call history commands ───────────────────────────────────────────────────
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1892,6 +1957,7 @@ pub fn run() {
|
||||
hangup_call,
|
||||
deregister,
|
||||
set_speakerphone, is_speakerphone_on,
|
||||
set_bluetooth_sco, is_bluetooth_available, get_audio_route,
|
||||
get_call_history, get_recent_contacts, clear_call_history,
|
||||
set_dred_verbose_logs, get_dred_verbose_logs,
|
||||
set_call_debug_logs, get_call_debug_logs,
|
||||
|
||||
Reference in New Issue
Block a user