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:
Siavash Sameni
2026-04-12 16:07:41 +04:00
parent 29cd23fe39
commit 4c1ad841e1
15 changed files with 1050 additions and 105 deletions

View File

@@ -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)
}