diff --git a/Cargo.toml b/Cargo.toml index 9624cd7..e254177 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ exclude = ["*.cfg", "*.yml"] edition = "2018" [lib] -crate-type = ["lib", "cdylib", "staticlib"] +crate-type = ["lib", "dylib", "staticlib"] [features] use_tolk = ["tolk"] diff --git a/examples/android/app/build.gradle b/examples/android/app/build.gradle index 8cefd06..2e99b3c 100644 --- a/examples/android/app/build.gradle +++ b/examples/android/app/build.gradle @@ -33,6 +33,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.core:core-ktx:1.2.0" + implementation "androidx.annotation:annotation:1.1.0" implementation "com.google.android.material:material:1.1.0" implementation "androidx.constraintlayout:constraintlayout:1.1.3" } diff --git a/examples/android/app/src/main/java/rs/tts/Bridge.java b/examples/android/app/src/main/java/rs/tts/Bridge.java new file mode 100644 index 0000000..4850c25 --- /dev/null +++ b/examples/android/app/src/main/java/rs/tts/Bridge.java @@ -0,0 +1,15 @@ +package rs.tts; + +import android.speech.tts.TextToSpeech; + +@androidx.annotation.Keep +public class Bridge implements TextToSpeech.OnInitListener { + public int backendId; + + public Bridge(int backendId) { + this.backendId = backendId; + } + + public native void onInit(int status); + +} \ No newline at end of file diff --git a/examples/android/app/src/main/java/rs/tts/MainActivity.kt b/examples/android/app/src/main/java/rs/tts/MainActivity.kt index 6b75440..0bba51f 100644 --- a/examples/android/app/src/main/java/rs/tts/MainActivity.kt +++ b/examples/android/app/src/main/java/rs/tts/MainActivity.kt @@ -1,15 +1,11 @@ package rs.tts import android.app.NativeActivity -import android.speech.tts.TextToSpeech -import android.speech.tts.TextToSpeech.OnInitListener -class MainActivity : NativeActivity(), OnInitListener { - override fun onInit(status:Int) { - if(status == TextToSpeech.SUCCESS) { - println("Successfully initialized TTS!") - } else { - println("Failed to initialize TTS.") +class MainActivity : NativeActivity() { + companion object { + init { + System.loadLibrary("hello_world") } } } \ No newline at end of file diff --git a/examples/android/cargo.toml b/examples/android/cargo.toml index a7ff289..2748088 100644 --- a/examples/android/cargo.toml +++ b/examples/android/cargo.toml @@ -10,5 +10,6 @@ edition = "2018" crate-type = ["dylib"] [dependencies] +jni = "0.18" ndk-glue = "0.2" tts = { path = "../.." } \ No newline at end of file diff --git a/examples/android/src/lib.rs b/examples/android/src/lib.rs index da4ef23..8a256d6 100644 --- a/examples/android/src/lib.rs +++ b/examples/android/src/lib.rs @@ -1,10 +1,7 @@ -use std::thread; -use std::time::Duration; use tts::*; fn run() -> Result<(), Error> { let mut tts = TTS::default()?; - thread::sleep(Duration::from_secs(5)); let Features { utterance_callbacks, .. @@ -65,6 +62,5 @@ fn run() -> Result<(), Error> { #[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] pub fn main() { - println!("xxxx In library"); run().expect("Failed to run"); } diff --git a/src/backends/android.rs b/src/backends/android.rs index 6a2e200..a70b054 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -1,15 +1,49 @@ #[cfg(target_os = "android")] -use std::sync::Mutex; +use std::collections::HashSet; +use std::os::raw::c_void; +use std::sync::{Mutex, RwLock}; +use std::thread; +use std::time::Duration; use jni::objects::{GlobalRef, JObject}; -use jni::JavaVM; +use jni::sys::{jint, JNI_VERSION_1_6}; +use jni::{JNIEnv, JavaVM}; use lazy_static::lazy_static; use log::info; use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; lazy_static! { + static ref BRIDGE: Mutex> = Mutex::new(None); static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); + static ref PENDING_INITIALIZATIONS: RwLock> = RwLock::new(HashSet::new()); +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn JNI_OnLoad(vm: JavaVM, _: *mut c_void) -> jint { + let env = vm.get_env().expect("Cannot get reference to the JNIEnv"); + let b = env + .find_class("rs/tts/Bridge") + .expect("Failed to find `Bridge`"); + let b = env + .new_global_ref(b) + .expect("Failed to create `Bridge` `GlobalRef`"); + let mut bridge = BRIDGE.lock().unwrap(); + *bridge = Some(b); + JNI_VERSION_1_6 +} + +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(env: JNIEnv, obj: JObject, status: jint) { + let id = env + .get_field(obj, "backendId", "I") + .expect("Failed to get backend ID") + .i() + .expect("Failed to cast to int") as u64; + let mut pending = PENDING_INITIALIZATIONS.write().unwrap(); + (*pending).remove(&id); } #[derive(Clone)] @@ -22,23 +56,40 @@ impl Android { pub(crate) fn new() -> Result { info!("Initializing Android backend"); let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); - let id = BackendId::Android(*backend_id); + let bid = *backend_id; + let id = BackendId::Android(bid); *backend_id += 1; + drop(backend_id); let native_activity = ndk_glue::native_activity(); let vm = Self::vm()?; let env = vm.attach_current_thread_permanently()?; - let tts = env.new_object( - "android/speech/tts/TextToSpeech", - "(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V", - &[ - native_activity.activity().into(), - native_activity.activity().into(), - ], - )?; - println!("Creating global ref"); - let tts = env.new_global_ref(tts)?; - println!("Returning"); - Ok(Self { id, tts }) + let bridge = BRIDGE.lock().unwrap(); + if let Some(bridge) = &*bridge { + let bridge = env.new_object(bridge, "(I)V", &[(bid as jint).into()])?; + let tts = env.new_object( + "android/speech/tts/TextToSpeech", + "(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V", + &[native_activity.activity().into(), bridge.into()], + )?; + { + let mut pending = PENDING_INITIALIZATIONS.write().unwrap(); + (*pending).insert(bid); + } + let tts = env.new_global_ref(tts)?; + // This hack makes my brain bleed. + loop { + { + let pending = PENDING_INITIALIZATIONS.read().unwrap(); + if !(*pending).contains(&bid) { + break; + } + } + thread::sleep(Duration::from_millis(5)); + } + Ok(Self { id, tts }) + } else { + Err(Error::NoneError) + } } fn vm() -> Result { @@ -65,15 +116,11 @@ impl Backend for Android { } fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { - println!("Speaking {}, {:?}", text, interrupt); let vm = Self::vm()?; - println!("Retrieved"); let env = vm.get_env()?; - println!("attached"); let tts = self.tts.as_obj(); let text = env.new_string(text)?; let queue_mode = if interrupt { 0 } else { 1 }; - println!("Calling"); env.call_method( tts, "speak", @@ -85,7 +132,6 @@ impl Backend for Android { JObject::null().into(), ], )?; - println!("Returning"); Ok(None) } @@ -94,15 +140,15 @@ impl Backend for Android { } fn min_rate(&self) -> f32 { - todo!() + 0.1 } fn max_rate(&self) -> f32 { - todo!() + 10. } fn normal_rate(&self) -> f32 { - todo!() + 1. } fn get_rate(&self) -> Result { @@ -114,15 +160,15 @@ impl Backend for Android { } fn min_pitch(&self) -> f32 { - todo!() + 0.1 } fn max_pitch(&self) -> f32 { - todo!() + 2. } fn normal_pitch(&self) -> f32 { - todo!() + 1. } fn get_pitch(&self) -> Result {