fix(audio): migrate Android JNI paths
This commit is contained in:
@@ -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)]
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user