fix(audio): migrate Android JNI paths

This commit is contained in:
Edison Jwa
2026-06-08 21:24:16 +09:00
parent 901369b072
commit 5a1d902795
3 changed files with 259 additions and 221 deletions
@@ -49,6 +49,7 @@ pub(crate) struct AndroidRenderRingConsumer {
} }
impl AndroidRenderRingConsumer { impl AndroidRenderRingConsumer {
#[cfg(test)]
pub(crate) fn drain_into_zero_filling(&self, out: &mut [f32]) { pub(crate) fn drain_into_zero_filling(&self, out: &mut [f32]) {
let mut chunks = out.chunks_exact_mut(2); let mut chunks = out.chunks_exact_mut(2);
for frame_out in &mut chunks { for frame_out in &mut chunks {
@@ -59,6 +60,13 @@ impl AndroidRenderRingConsumer {
*sample = 0.0; *sample = 0.0;
} }
} }
pub(crate) fn drain_stereo_into_zero_filling(&self, out: &mut [(f32, f32)]) {
for frame_out in out {
let frame = self.frames.pop().unwrap_or([0.0, 0.0]);
*frame_out = (frame[0], frame[1]);
}
}
} }
#[cfg(test)] #[cfg(test)]
+185 -173
View File
@@ -516,24 +516,39 @@ impl AudioOutputCallback for OutputCallback {
frames: &mut [(f32, f32)], frames: &mut [(f32, f32)],
) -> DataCallbackResult { ) -> DataCallbackResult {
let _ = catch_unwind(AssertUnwindSafe(|| { let _ = catch_unwind(AssertUnwindSafe(|| {
let buf: &mut [f32] = bytemuck::cast_slice_mut::<(f32, f32), f32>(frames); self.pcm_consumer.drain_stereo_into_zero_filling(frames);
self.pcm_consumer.drain_into_zero_filling(buf);
let gain = f32::from_bits(self.output_gain.load(Ordering::Relaxed)); let gain = f32::from_bits(self.output_gain.load(Ordering::Relaxed));
let muted = self.output_muted.load(Ordering::Relaxed); let muted = self.output_muted.load(Ordering::Relaxed);
if muted { if muted {
for s in buf.iter_mut() { for frame in frames.iter_mut() {
*s = 0.0; *frame = (0.0, 0.0);
} }
} else if gain != 1.0 { } else if gain != 1.0 {
for s in buf.iter_mut() { for (left, right) in frames.iter_mut() {
*s *= gain; *left *= gain;
*right *= gain;
} }
} }
let mut sum_squares = 0.0_f32;
for (left, right) in frames.iter() {
sum_squares += left * left + right * right;
}
let sample_count = frames.len() * 2;
let dbfs = if sample_count == 0 {
-120.0
} else {
let rms = (sum_squares / sample_count as f32).sqrt();
if rms <= 0.000_001 {
-120.0
} else {
20.0 * rms.log10()
}
};
self.audio_processing_stats self.audio_processing_stats
.update_render(crate::frame::dbfs(buf), frames.len() as u32); .update_render(dbfs, frames.len() as u32);
for chunk in buf.chunks_exact(2) { for (left, right) in frames.iter() {
self.pending_render_ref[self.pending_render_ref_len] = (chunk[0] + chunk[1]) * 0.5; self.pending_render_ref[self.pending_render_ref_len] = (left + right) * 0.5;
self.pending_render_ref_len += 1; self.pending_render_ref_len += 1;
if self.pending_render_ref_len == crate::frame::FRAME_10MS_SAMPLES { if self.pending_render_ref_len == crate::frame::FRAME_10MS_SAMPLES {
self.render_reference.write(&self.pending_render_ref); self.render_reference.write(&self.pending_render_ref);
@@ -587,11 +602,13 @@ pub struct AndroidVoiceUnit {
#[derive(Default)] #[derive(Default)]
struct HardwareEffectHandles { struct HardwareEffectHandles {
aec: Option<jni::objects::GlobalRef>, aec: Option<AndroidGlobalObject>,
ns: Option<jni::objects::GlobalRef>, ns: Option<AndroidGlobalObject>,
agc: Option<jni::objects::GlobalRef>, agc: Option<AndroidGlobalObject>,
} }
type AndroidGlobalObject = jni::refs::Global<jni::objects::JObject<'static>>;
impl AndroidVoiceUnit { impl AndroidVoiceUnit {
/// Open the input + output streams (SDD-111 + SDD-112) and, /// Open the input + output streams (SDD-111 + SDD-112) and,
/// once a session id is available, attach SDD-113 hardware /// once a session id is available, attach SDD-113 hardware
@@ -1297,62 +1314,71 @@ fn attach_hardware_effects_inner(
session_id: AudioSessionId, session_id: AudioSessionId,
effects: &crate::AudioEffects, effects: &crate::AudioEffects,
) -> HardwareEffectHandles { ) -> HardwareEffectHandles {
with_android_env("hardware effects", |env| {
let mut handles = HardwareEffectHandles::default();
if effects.aec {
handles.aec = create_effect(
env,
"android/media/audiofx/AcousticEchoCanceler",
session_id,
"AEC",
);
}
if effects.noise_suppression {
handles.ns = create_effect(
env,
"android/media/audiofx/NoiseSuppressor",
session_id,
"NS",
);
}
if effects.agc {
handles.agc = create_effect(
env,
"android/media/audiofx/AutomaticGainControl",
session_id,
"AGC",
);
}
handles
})
.unwrap_or_default()
}
fn with_android_env<R>(
operation: &str,
op: impl for<'local> FnOnce(&mut jni::Env<'local>) -> R,
) -> Option<R> {
let ctx = ndk_context::android_context(); let ctx = ndk_context::android_context();
if ctx.vm().is_null() { if ctx.vm().is_null() {
warn!( warn!(
target: "chanora_audio", target: "chanora_audio",
"android: ndk_context vm null; cannot bind hardware effects (software fallback engages)" operation,
"android: ndk_context vm null; JNI call skipped"
); );
return HardwareEffectHandles::default(); return None;
} }
let jvm = match unsafe { jni::JavaVM::from_raw(ctx.vm() as *mut _) } {
Ok(v) => v,
Err(e) => {
warn!(target: "chanora_audio", error = %e, "android: JavaVM::from_raw failed; effects not bound");
return HardwareEffectHandles::default();
}
};
let mut env = match jvm.attach_current_thread() {
Ok(e) => e,
Err(e) => {
warn!(target: "chanora_audio", error = %e, "android: attach_current_thread failed; effects not bound");
return HardwareEffectHandles::default();
}
};
let mut handles = HardwareEffectHandles::default(); let jvm = unsafe { jni::JavaVM::from_raw(ctx.vm() as *mut _) };
if effects.aec { match jvm.attach_current_thread(|env| Ok::<R, jni::errors::Error>(op(env))) {
handles.aec = create_effect( Ok(value) => Some(value),
&mut env, Err(e) => {
"android/media/audiofx/AcousticEchoCanceler", warn!(target: "chanora_audio", error = %e, operation, "android: attach_current_thread failed");
session_id, None
"AEC", }
);
} }
if effects.noise_suppression {
handles.ns = create_effect(
&mut env,
"android/media/audiofx/NoiseSuppressor",
session_id,
"NS",
);
}
if effects.agc {
handles.agc = create_effect(
&mut env,
"android/media/audiofx/AutomaticGainControl",
session_id,
"AGC",
);
}
handles
} }
/// SDD-113 item 3: probe the static `isAvailable()` on each effect /// SDD-113 item 3: probe the static `isAvailable()` on each effect
/// class before calling `create(int)`. Returns `false` on any JNI /// class before calling `create(int)`. Returns `false` on any JNI
/// failure so the caller engages the software fallback. /// failure so the caller engages the software fallback.
fn effect_is_available(env: &mut jni::JNIEnv, class: &jni::objects::JClass, label: &str) -> bool { fn effect_is_available(env: &mut jni::Env<'_>, class: &jni::objects::JClass, label: &str) -> bool {
match env.call_static_method(class, "isAvailable", "()Z", &[]) { match env.call_static_method(
class,
jni::jni_str!("isAvailable"),
jni::jni_sig!("()Z"),
&[],
) {
Ok(v) => match v.z() { Ok(v) => match v.z() {
Ok(b) => b, Ok(b) => b,
Err(e) => { Err(e) => {
@@ -1370,14 +1396,14 @@ fn effect_is_available(env: &mut jni::JNIEnv, class: &jni::objects::JClass, labe
} }
fn create_effect( fn create_effect(
env: &mut jni::JNIEnv, env: &mut jni::Env<'_>,
fqcn: &str, fqcn: &str,
session_id: AudioSessionId, session_id: AudioSessionId,
label: &str, label: &str,
) -> Option<jni::objects::GlobalRef> { ) -> Option<AndroidGlobalObject> {
use jni::objects::JValue; use jni::objects::JValue;
// Class.create(int) -> ClassInstance|null // Class.create(int) -> ClassInstance|null
let class = match env.find_class(fqcn) { let class = match env.find_class(jni::strings::JNIString::new(fqcn)) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
warn!(target: "chanora_audio", error = %e, effect = label, "android: find_class failed; effect not bound — software fallback engages"); warn!(target: "chanora_audio", error = %e, effect = label, "android: find_class failed; effect not bound — software fallback engages");
@@ -1393,10 +1419,18 @@ fn create_effect(
); );
return None; return None;
} }
let create_sig = match jni::signature::RuntimeMethodSignature::from_str(format!("(I)L{fqcn};"))
{
Ok(sig) => sig,
Err(e) => {
warn!(target: "chanora_audio", error = %e, effect = label, "android: create() signature parse failed");
return None;
}
};
let inst = match env.call_static_method( let inst = match env.call_static_method(
&class, &class,
"create", jni::jni_str!("create"),
&format!("(I)L{fqcn};"), create_sig.method_signature(),
&[JValue::Int(session_id)], &[JValue::Int(session_id)],
) { ) {
Ok(v) => match v.l() { Ok(v) => match v.l() {
@@ -1421,8 +1455,8 @@ fn create_effect(
// setEnabled(true) -> int (success code) // setEnabled(true) -> int (success code)
if let Err(e) = env.call_method( if let Err(e) = env.call_method(
&inst, &inst,
"setEnabled", jni::jni_str!("setEnabled"),
"(Z)I", jni::jni_sig!("(Z)I"),
&[JValue::Bool(jni::sys::JNI_TRUE)], &[JValue::Bool(jni::sys::JNI_TRUE)],
) { ) {
let _ = env.exception_clear(); let _ = env.exception_clear();
@@ -1458,34 +1492,28 @@ fn release_hardware_effects_inner(handles: &mut HardwareEffectHandles) {
if aec.is_none() && ns.is_none() && agc.is_none() { if aec.is_none() && ns.is_none() && agc.is_none() {
return; return;
} }
let ctx = ndk_context::android_context(); let _ = with_android_env("release hardware effects", |env| {
if ctx.vm().is_null() { for (effect, label) in [(aec, "AEC"), (ns, "NS"), (agc, "AGC")] {
return; if let Some(g) = effect {
} let _ = env.call_method(
// SAFETY: vm is non-null and owned for process lifetime via JNI_OnLoad. g.as_obj(),
let jvm = match unsafe { jni::JavaVM::from_raw(ctx.vm() as *mut _) } { jni::jni_str!("setEnabled"),
Ok(v) => v, jni::jni_sig!("(Z)I"),
Err(_) => return, &[jni::objects::JValue::Bool(jni::sys::JNI_FALSE)],
}; );
let mut env = match jvm.attach_current_thread() { env.exception_clear();
Ok(e) => e, let _ = env.call_method(
Err(_) => return, g.as_obj(),
}; jni::jni_str!("release"),
for (effect, label) in [(aec, "AEC"), (ns, "NS"), (agc, "AGC")] { jni::jni_sig!("()V"),
if let Some(g) = effect { &[],
let _ = env.call_method( );
g.as_obj(), env.exception_clear();
"setEnabled", drop(g);
"(Z)I", info!(target: "chanora_audio", effect = label, "android: hardware effect released");
&[jni::objects::JValue::Bool(jni::sys::JNI_FALSE)], }
);
let _ = env.exception_clear();
let _ = env.call_method(g.as_obj(), "release", "()V", &[]);
let _ = env.exception_clear();
drop(g);
info!(target: "chanora_audio", effect = label, "android: hardware effect released");
} }
} });
} }
// --- Process-global BackendEvent sender for JNI callbacks -------- // --- Process-global BackendEvent sender for JNI callbacks --------
@@ -1560,7 +1588,7 @@ pub fn chanora_android_stop_voice_service() -> bool {
fn call_voice_service_static(method: &str) -> bool { fn call_voice_service_static(method: &str) -> bool {
use jni::objects::{JObject, JValue}; use jni::objects::{JObject, JValue};
let ctx = ndk_context::android_context(); let ctx = ndk_context::android_context();
if ctx.vm().is_null() || ctx.context().is_null() { if ctx.context().is_null() {
warn!( warn!(
target: "chanora_audio", target: "chanora_audio",
method, method,
@@ -1568,54 +1596,41 @@ fn call_voice_service_static(method: &str) -> bool {
); );
return false; return false;
} }
// SAFETY: vm/context populated by chanora_bridge::android_init at
// JNI_OnLoad + initChanoraContext; both pointers are valid for with_android_env("voice foreground service", |env| {
// the process lifetime. // SAFETY: ndk_context::context() is the application Context
let jvm = match unsafe { jni::JavaVM::from_raw(ctx.vm() as *mut _) } { // jobject; valid global ref for process lifetime.
Ok(v) => v, let context_obj = unsafe { JObject::from_raw(env, ctx.context() as jni::sys::jobject) };
Err(e) => { let class = match load_app_class(env, &context_obj, ANDROID_VOICE_FG_SERVICE_FQCN) {
warn!(target: "chanora_audio", error = %e, method, "android: JavaVM::from_raw failed"); Some(c) => c,
return false; None => return false,
};
match env.call_static_method(
&class,
jni::strings::JNIString::new(method),
jni::jni_sig!("(Landroid/content/Context;)V"),
&[JValue::Object(&context_obj)],
) {
Ok(_) => {
info!(target: "chanora_audio", method, "android: voice foreground service call dispatched");
true
}
Err(e) => {
env.exception_clear();
warn!(target: "chanora_audio", error = %e, method, "android: foreground service static call failed");
false
}
} }
}; })
let mut env = match jvm.attach_current_thread() { .unwrap_or(false)
Ok(e) => e,
Err(e) => {
warn!(target: "chanora_audio", error = %e, method, "android: attach_current_thread failed");
return false;
}
};
// SAFETY: ndk_context::context() is the application Context
// jobject; valid global ref for process lifetime.
let context_obj = unsafe { JObject::from_raw(ctx.context() as jni::sys::jobject) };
let class = match load_app_class(&mut env, &context_obj, ANDROID_VOICE_FG_SERVICE_FQCN) {
Some(c) => c,
None => return false,
};
match env.call_static_method(
&class,
method,
"(Landroid/content/Context;)V",
&[JValue::Object(&context_obj)],
) {
Ok(_) => {
info!(target: "chanora_audio", method, "android: voice foreground service call dispatched");
true
}
Err(e) => {
let _ = env.exception_clear();
warn!(target: "chanora_audio", error = %e, method, "android: foreground service static call failed");
false
}
}
} }
fn load_app_class<'local>( fn load_app_class<'local>(
env: &mut jni::JNIEnv<'local>, env: &mut jni::Env<'local>,
context_obj: &jni::objects::JObject<'local>, context_obj: &jni::objects::JObject<'local>,
slash_name: &str, slash_name: &str,
) -> Option<jni::objects::JClass<'local>> { ) -> Option<jni::objects::JClass<'local>> {
match env.find_class(slash_name) { match env.find_class(jni::strings::JNIString::new(slash_name)) {
Ok(c) => return Some(c), Ok(c) => return Some(c),
Err(e) => { Err(e) => {
let _ = env.exception_clear(); let _ = env.exception_clear();
@@ -1626,8 +1641,8 @@ fn load_app_class<'local>(
let loader = match env let loader = match env
.call_method( .call_method(
context_obj, context_obj,
"getClassLoader", jni::jni_str!("getClassLoader"),
"()Ljava/lang/ClassLoader;", jni::jni_sig!("()Ljava/lang/ClassLoader;"),
&[], &[],
) )
.and_then(|v| v.l()) .and_then(|v| v.l())
@@ -1652,13 +1667,20 @@ fn load_app_class<'local>(
match env match env
.call_method( .call_method(
&loader, &loader,
"loadClass", jni::jni_str!("loadClass"),
"(Ljava/lang/String;)Ljava/lang/Class;", jni::jni_sig!("(Ljava/lang/String;)Ljava/lang/Class;"),
&[jni::objects::JValue::Object(&class_name_obj)], &[jni::objects::JValue::Object(&class_name_obj)],
) )
.and_then(|v| v.l()) .and_then(|v| v.l())
{ {
Ok(class_obj) => Some(jni::objects::JClass::from(class_obj)), Ok(class_obj) => match env.cast_local::<jni::objects::JClass>(class_obj) {
Ok(class) => Some(class),
Err(e) => {
env.exception_clear();
warn!(target: "chanora_audio", error = %e, class = %dotted_name, "android: ClassLoader.loadClass returned non-Class object");
None
}
},
Err(e) => { Err(e) => {
let _ = env.exception_clear(); let _ = env.exception_clear();
warn!(target: "chanora_audio", error = %e, class = %dotted_name, "android: ClassLoader.loadClass failed"); warn!(target: "chanora_audio", error = %e, class = %dotted_name, "android: ClassLoader.loadClass failed");
@@ -1689,7 +1711,7 @@ fn load_app_class<'local>(
pub extern "system" fn Java_app_chanora_chanora_1flutter_AndroidAudioFocusController_publishFocusChange< pub extern "system" fn Java_app_chanora_chanora_1flutter_AndroidAudioFocusController_publishFocusChange<
'local, 'local,
>( >(
_env: jni::JNIEnv<'local>, _env: jni::EnvUnowned<'local>,
_class: jni::objects::JClass<'local>, _class: jni::objects::JClass<'local>,
state: jni::sys::jint, state: jni::sys::jint,
) { ) {
@@ -1727,7 +1749,7 @@ pub extern "system" fn Java_app_chanora_chanora_1flutter_AndroidAudioFocusContro
pub extern "system" fn Java_app_chanora_chanora_1flutter_AndroidBluetoothScoController_publishScoStateChange< pub extern "system" fn Java_app_chanora_chanora_1flutter_AndroidBluetoothScoController_publishScoStateChange<
'local, 'local,
>( >(
_env: jni::JNIEnv<'local>, _env: jni::EnvUnowned<'local>,
_class: jni::objects::JClass<'local>, _class: jni::objects::JClass<'local>,
state: jni::sys::jint, state: jni::sys::jint,
) { ) {
@@ -1776,7 +1798,7 @@ pub fn chanora_android_stop_bluetooth_sco() -> bool {
fn call_static_void_context(fqcn: &str, method: &str) -> bool { fn call_static_void_context(fqcn: &str, method: &str) -> bool {
use jni::objects::{JObject, JValue}; use jni::objects::{JObject, JValue};
let ctx = ndk_context::android_context(); let ctx = ndk_context::android_context();
if ctx.vm().is_null() || ctx.context().is_null() { if ctx.context().is_null() {
warn!( warn!(
target: "chanora_audio", target: "chanora_audio",
class = fqcn, class = fqcn,
@@ -1785,39 +1807,29 @@ fn call_static_void_context(fqcn: &str, method: &str) -> bool {
); );
return false; return false;
} }
let jvm = match unsafe { jni::JavaVM::from_raw(ctx.vm() as *mut _) } {
Ok(v) => v, with_android_env("static context call", |env| {
Err(e) => { let context_obj = unsafe { JObject::from_raw(env, ctx.context() as jni::sys::jobject) };
warn!(target: "chanora_audio", error = %e, class = fqcn, method, "android: JavaVM::from_raw failed"); let class = match load_app_class(env, &context_obj, fqcn) {
return false; Some(c) => c,
None => return false,
};
match env.call_static_method(
&class,
jni::strings::JNIString::new(method),
jni::jni_sig!("(Landroid/content/Context;)V"),
&[JValue::Object(&context_obj)],
) {
Ok(_) => {
info!(target: "chanora_audio", class = fqcn, method, "android: dispatched");
true
}
Err(e) => {
env.exception_clear();
warn!(target: "chanora_audio", error = %e, class = fqcn, method, "android: static call failed");
false
}
} }
}; })
let mut env = match jvm.attach_current_thread() { .unwrap_or(false)
Ok(e) => e,
Err(e) => {
warn!(target: "chanora_audio", error = %e, class = fqcn, method, "android: attach_current_thread failed");
return false;
}
};
let context_obj = unsafe { JObject::from_raw(ctx.context() as jni::sys::jobject) };
let class = match load_app_class(&mut env, &context_obj, fqcn) {
Some(c) => c,
None => return false,
};
match env.call_static_method(
&class,
method,
"(Landroid/content/Context;)V",
&[JValue::Object(&context_obj)],
) {
Ok(_) => {
info!(target: "chanora_audio", class = fqcn, method, "android: dispatched");
true
}
Err(e) => {
let _ = env.exception_clear();
warn!(target: "chanora_audio", error = %e, class = fqcn, method, "android: static call failed");
false
}
}
} }
+66 -48
View File
@@ -2448,6 +2448,13 @@ impl std::fmt::Display for AudioModeError {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
impl std::error::Error for AudioModeError {} impl std::error::Error for AudioModeError {}
#[cfg(target_os = "android")]
impl From<jni::errors::Error> for AudioModeError {
fn from(value: jni::errors::Error) -> Self {
Self::JniAttachFailed(value.to_string())
}
}
/// JNI helper shared by `android_get_audio_mode` and /// JNI helper shared by `android_get_audio_mode` and
/// `android_set_audio_mode`: attach to the current thread and return /// `android_set_audio_mode`: attach to the current thread and return
/// the `AudioManager` jobject. Centralised so SDD-108's two platform /// the `AudioManager` jobject. Centralised so SDD-108's two platform
@@ -2455,7 +2462,10 @@ impl std::error::Error for AudioModeError {}
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
fn android_audio_manager_call<F, R>(op: F) -> Result<R, AudioModeError> fn android_audio_manager_call<F, R>(op: F) -> Result<R, AudioModeError>
where where
F: FnOnce(&mut jni::JNIEnv, &jni::objects::JObject) -> Result<R, AudioModeError> F: for<'local> FnOnce(
&mut jni::Env<'local>,
&jni::objects::JObject<'local>,
) -> Result<R, AudioModeError>
+ std::panic::UnwindSafe, + std::panic::UnwindSafe,
{ {
use jni::objects::{JObject, JString, JValue}; use jni::objects::{JObject, JString, JValue};
@@ -2472,42 +2482,40 @@ where
// at a live JavaVM* set by our bridge_init JNI hook. The // at a live JavaVM* set by our bridge_init JNI hook. The
// unsafe block contains only the cast required by // unsafe block contains only the cast required by
// `JavaVM::from_raw`. // `JavaVM::from_raw`.
let jvm = unsafe { jni::JavaVM::from_raw(vm_ptr as *mut _) } let jvm = unsafe { jni::JavaVM::from_raw(vm_ptr as *mut _) };
.map_err(|e| AudioModeError::JniAttachFailed(format!("jvm from_raw: {e}")))?; jvm.attach_current_thread(|env| -> Result<R, AudioModeError> {
let mut env = jvm let context_obj = unsafe { JObject::from_raw(env, ctx.context() as jni::sys::jobject) };
.attach_current_thread()
.map_err(|e| AudioModeError::JniAttachFailed(format!("attach: {e}")))?;
let context_obj = unsafe { JObject::from_raw(ctx.context() as jni::sys::jobject) }; let service_name: JString =
env.new_string("audio")
let service_name: JString = .map_err(|e| AudioModeError::MethodCallFailed {
env.new_string("audio") method: "new_string",
detail: e.to_string(),
})?;
let service_name_obj = JObject::from(service_name);
let audio_manager = env
.call_method(
&context_obj,
jni::jni_str!("getSystemService"),
jni::jni_sig!("(Ljava/lang/String;)Ljava/lang/Object;"),
&[JValue::Object(&service_name_obj)],
)
.map_err(|e| AudioModeError::MethodCallFailed { .map_err(|e| AudioModeError::MethodCallFailed {
method: "new_string", method: "getSystemService",
detail: e.to_string(), detail: e.to_string(),
})?
.l()
.map_err(|e| AudioModeError::MethodCallFailed {
method: "getSystemService",
detail: format!("obj cast: {e}"),
})?; })?;
let audio_manager = env if audio_manager.is_null() {
.call_method( return Err(AudioModeError::Other(
&context_obj, "AudioManager service is null".to_string(),
"getSystemService", ));
"(Ljava/lang/String;)Ljava/lang/Object;", }
&[JValue::Object(&service_name.into())], op(env, &audio_manager)
) })
.map_err(|e| AudioModeError::MethodCallFailed {
method: "getSystemService",
detail: e.to_string(),
})?
.l()
.map_err(|e| AudioModeError::MethodCallFailed {
method: "getSystemService",
detail: format!("obj cast: {e}"),
})?;
if audio_manager.is_null() {
return Err(AudioModeError::Other(
"AudioManager service is null".to_string(),
));
}
op(&mut env, &audio_manager)
}); });
match result { match result {
Ok(inner) => inner, Ok(inner) => inner,
@@ -2526,16 +2534,21 @@ where
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
pub fn android_get_audio_mode() -> Result<i32, AudioModeError> { pub fn android_get_audio_mode() -> Result<i32, AudioModeError> {
android_audio_manager_call(|env, audio_manager| { android_audio_manager_call(|env, audio_manager| {
env.call_method(audio_manager, "getMode", "()I", &[]) env.call_method(
.map_err(|e| AudioModeError::MethodCallFailed { audio_manager,
method: "getMode", jni::jni_str!("getMode"),
detail: e.to_string(), jni::jni_sig!("()I"),
})? &[],
.i() )
.map_err(|e| AudioModeError::MethodCallFailed { .map_err(|e| AudioModeError::MethodCallFailed {
method: "getMode", method: "getMode",
detail: format!("int cast: {e}"), detail: e.to_string(),
}) })?
.i()
.map_err(|e| AudioModeError::MethodCallFailed {
method: "getMode",
detail: format!("int cast: {e}"),
})
}) })
} }
@@ -2549,11 +2562,16 @@ pub fn android_get_audio_mode() -> Result<i32, AudioModeError> {
pub fn android_set_audio_mode(mode: i32) -> Result<(), AudioModeError> { pub fn android_set_audio_mode(mode: i32) -> Result<(), AudioModeError> {
use jni::objects::JValue; use jni::objects::JValue;
android_audio_manager_call(move |env, audio_manager| { android_audio_manager_call(move |env, audio_manager| {
env.call_method(audio_manager, "setMode", "(I)V", &[JValue::Int(mode)]) env.call_method(
.map_err(|e| AudioModeError::MethodCallFailed { audio_manager,
method: "setMode", jni::jni_str!("setMode"),
detail: e.to_string(), jni::jni_sig!("(I)V"),
})?; &[JValue::Int(mode)],
)
.map_err(|e| AudioModeError::MethodCallFailed {
method: "setMode",
detail: e.to_string(),
})?;
Ok(()) Ok(())
}) })
} }