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)]
+129 -117
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,33 +1314,11 @@ fn attach_hardware_effects_inner(
session_id: AudioSessionId, session_id: AudioSessionId,
effects: &crate::AudioEffects, effects: &crate::AudioEffects,
) -> HardwareEffectHandles { ) -> HardwareEffectHandles {
let ctx = ndk_context::android_context(); with_android_env("hardware effects", |env| {
if ctx.vm().is_null() {
warn!(
target: "chanora_audio",
"android: ndk_context vm null; cannot bind hardware effects (software fallback engages)"
);
return HardwareEffectHandles::default();
}
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 mut handles = HardwareEffectHandles::default();
if effects.aec { if effects.aec {
handles.aec = create_effect( handles.aec = create_effect(
&mut env, env,
"android/media/audiofx/AcousticEchoCanceler", "android/media/audiofx/AcousticEchoCanceler",
session_id, session_id,
"AEC", "AEC",
@@ -1331,7 +1326,7 @@ fn attach_hardware_effects_inner(
} }
if effects.noise_suppression { if effects.noise_suppression {
handles.ns = create_effect( handles.ns = create_effect(
&mut env, env,
"android/media/audiofx/NoiseSuppressor", "android/media/audiofx/NoiseSuppressor",
session_id, session_id,
"NS", "NS",
@@ -1339,20 +1334,51 @@ fn attach_hardware_effects_inner(
} }
if effects.agc { if effects.agc {
handles.agc = create_effect( handles.agc = create_effect(
&mut env, env,
"android/media/audiofx/AutomaticGainControl", "android/media/audiofx/AutomaticGainControl",
session_id, session_id,
"AGC", "AGC",
); );
} }
handles 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();
if ctx.vm().is_null() {
warn!(
target: "chanora_audio",
operation,
"android: ndk_context vm null; JNI call skipped"
);
return None;
}
let jvm = unsafe { jni::JavaVM::from_raw(ctx.vm() as *mut _) };
match jvm.attach_current_thread(|env| Ok::<R, jni::errors::Error>(op(env))) {
Ok(value) => Some(value),
Err(e) => {
warn!(target: "chanora_audio", error = %e, operation, "android: attach_current_thread failed");
None
}
}
} }
/// 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() {
return;
}
// SAFETY: vm is non-null and owned for process lifetime via JNI_OnLoad.
let jvm = match unsafe { jni::JavaVM::from_raw(ctx.vm() as *mut _) } {
Ok(v) => v,
Err(_) => return,
};
let mut env = match jvm.attach_current_thread() {
Ok(e) => e,
Err(_) => return,
};
for (effect, label) in [(aec, "AEC"), (ns, "NS"), (agc, "AGC")] { for (effect, label) in [(aec, "AEC"), (ns, "NS"), (agc, "AGC")] {
if let Some(g) = effect { if let Some(g) = effect {
let _ = env.call_method( let _ = env.call_method(
g.as_obj(), g.as_obj(),
"setEnabled", jni::jni_str!("setEnabled"),
"(Z)I", jni::jni_sig!("(Z)I"),
&[jni::objects::JValue::Bool(jni::sys::JNI_FALSE)], &[jni::objects::JValue::Bool(jni::sys::JNI_FALSE)],
); );
let _ = env.exception_clear(); env.exception_clear();
let _ = env.call_method(g.as_obj(), "release", "()V", &[]); let _ = env.call_method(
let _ = env.exception_clear(); g.as_obj(),
jni::jni_str!("release"),
jni::jni_sig!("()V"),
&[],
);
env.exception_clear();
drop(g); drop(g);
info!(target: "chanora_audio", effect = label, "android: hardware effect released"); 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,34 +1596,19 @@ 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.
let jvm = match unsafe { jni::JavaVM::from_raw(ctx.vm() as *mut _) } {
Ok(v) => v,
Err(e) => {
warn!(target: "chanora_audio", error = %e, method, "android: JavaVM::from_raw failed");
return false;
}
};
let mut env = match jvm.attach_current_thread() {
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 // SAFETY: ndk_context::context() is the application Context
// jobject; valid global ref for process lifetime. // jobject; valid global ref for process lifetime.
let context_obj = unsafe { JObject::from_raw(ctx.context() as jni::sys::jobject) }; let context_obj = unsafe { JObject::from_raw(env, ctx.context() as jni::sys::jobject) };
let class = match load_app_class(&mut env, &context_obj, ANDROID_VOICE_FG_SERVICE_FQCN) { let class = match load_app_class(env, &context_obj, ANDROID_VOICE_FG_SERVICE_FQCN) {
Some(c) => c, Some(c) => c,
None => return false, None => return false,
}; };
match env.call_static_method( match env.call_static_method(
&class, &class,
method, jni::strings::JNIString::new(method),
"(Landroid/content/Context;)V", jni::jni_sig!("(Landroid/content/Context;)V"),
&[JValue::Object(&context_obj)], &[JValue::Object(&context_obj)],
) { ) {
Ok(_) => { Ok(_) => {
@@ -1603,19 +1616,21 @@ fn call_voice_service_static(method: &str) -> bool {
true true
} }
Err(e) => { Err(e) => {
let _ = env.exception_clear(); env.exception_clear();
warn!(target: "chanora_audio", error = %e, method, "android: foreground service static call failed"); warn!(target: "chanora_audio", error = %e, method, "android: foreground service static call failed");
false false
} }
} }
})
.unwrap_or(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,29 +1807,17 @@ 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;
}
};
let mut env = match jvm.attach_current_thread() {
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, Some(c) => c,
None => return false, None => return false,
}; };
match env.call_static_method( match env.call_static_method(
&class, &class,
method, jni::strings::JNIString::new(method),
"(Landroid/content/Context;)V", jni::jni_sig!("(Landroid/content/Context;)V"),
&[JValue::Object(&context_obj)], &[JValue::Object(&context_obj)],
) { ) {
Ok(_) => { Ok(_) => {
@@ -1815,9 +1825,11 @@ fn call_static_void_context(fqcn: &str, method: &str) -> bool {
true true
} }
Err(e) => { Err(e) => {
let _ = env.exception_clear(); env.exception_clear();
warn!(target: "chanora_audio", error = %e, class = fqcn, method, "android: static call failed"); warn!(target: "chanora_audio", error = %e, class = fqcn, method, "android: static call failed");
false false
} }
} }
})
.unwrap_or(false)
} }
+32 -14
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,13 +2482,9 @@ 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 = let service_name: JString =
env.new_string("audio") env.new_string("audio")
@@ -2486,12 +2492,13 @@ where
method: "new_string", method: "new_string",
detail: e.to_string(), detail: e.to_string(),
})?; })?;
let service_name_obj = JObject::from(service_name);
let audio_manager = env let audio_manager = env
.call_method( .call_method(
&context_obj, &context_obj,
"getSystemService", jni::jni_str!("getSystemService"),
"(Ljava/lang/String;)Ljava/lang/Object;", jni::jni_sig!("(Ljava/lang/String;)Ljava/lang/Object;"),
&[JValue::Object(&service_name.into())], &[JValue::Object(&service_name_obj)],
) )
.map_err(|e| AudioModeError::MethodCallFailed { .map_err(|e| AudioModeError::MethodCallFailed {
method: "getSystemService", method: "getSystemService",
@@ -2507,7 +2514,8 @@ where
"AudioManager service is null".to_string(), "AudioManager service is null".to_string(),
)); ));
} }
op(&mut env, &audio_manager) op(env, &audio_manager)
})
}); });
match result { match result {
Ok(inner) => inner, Ok(inner) => inner,
@@ -2526,7 +2534,12 @@ 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(
audio_manager,
jni::jni_str!("getMode"),
jni::jni_sig!("()I"),
&[],
)
.map_err(|e| AudioModeError::MethodCallFailed { .map_err(|e| AudioModeError::MethodCallFailed {
method: "getMode", method: "getMode",
detail: e.to_string(), detail: e.to_string(),
@@ -2549,7 +2562,12 @@ 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(
audio_manager,
jni::jni_str!("setMode"),
jni::jni_sig!("(I)V"),
&[JValue::Int(mode)],
)
.map_err(|e| AudioModeError::MethodCallFailed { .map_err(|e| AudioModeError::MethodCallFailed {
method: "setMode", method: "setMode",
detail: e.to_string(), detail: e.to_string(),