-
-
Notifications
You must be signed in to change notification settings - Fork 158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Android Support #8
Comments
Hi - I see this sits on Jan 11. How can I help? |
may be worth looking into using https://github.com/jni-rs/jni-rs when pulling in blurdroid, it currently appears to use rust -(ffi)-> c -(jni)-> java wrapper -> android bluetooth. I'd think we'd be able to strip out the C layer, at least. |
I intend to work on this, because I want it for one of my own projects. I looked into blurdroid, and I see three problems with it:
Changing these three things effectively amounts to a complete rewrite. Blurdroid will certainly serve as a useful reference, but I believe it will be easier to start from scratch. |
@gedgygedgy You'll definitely want to work off the dev branch too, which has an API completely reworked for rust async. It's 100+ commits ahead of master, and we'll hopefully be merging it with master soon, so consider it the place to start. |
The new async API definitely changes things, and will probably make it easier to port to Android. One problem I see is that doing anything Java-related in Rust requires a
Using a global init approach limits the library to a single Java VM (which isn't a problem in Android, since Android only allows one VM per process anyway), requires the client to call the init function if it already implements Neither option is very appealing. What are your thoughts? Any other ideas on how to make this work? |
Personally, I would prefer to add a parameter to |
personally, I'm more inclined to go the route of the global variable than I am of adding an argument to the manager, just to reduce the amount of it also means that you can have a library A, which uses btleplug and doesn't care about android at all, and application B that runs on android, which uses library A, and application B can just pass the JavaVM to btleplug, still without needing library A to care at all. |
After seeing how easy I've started working on the Android port. You can check out my progress here. |
I've created a small crate, jni-utils-rs, to do some extra stuff that jni-rs doesn't do, specifically async and futures. I'm using this as the foundation of my Android port. |
Upon thinking about this some more, it doesn't seem very Rustacean to provide an API that can fail so easily. I will go forward with a command queue to make sure only one command gets executed at a time. |
Well if it makes you feel any better, I was gonna say command queue too. It's what I usually do in this case. :) |
I have a working |
WOOOOOOOOOOOOOOOOOOOO 👍🏻 💯 🥇 🚀 |
I've added a command queue to ensure commands execute one at a time. |
At this point I think I've implemented all of the functionality for Android. Scanning, A few caveats:
|
it seems like tokio has a couple of things that might help with this and not be too much of a hassle (in the case where you attach every thread to the jvm at least): https://docs.rs/tokio/1.7.1/tokio/runtime/struct.Builder.html#method.on_thread_start and https://docs.rs/tokio/1.7.1/tokio/runtime/struct.Builder.html#method.on_thread_stop |
What's the status on this? Did any of these changes make it into 0.9? |
This is currently on hold. The original author went quite in mid-August 2021, and I haven't been able to get ahold of them since. |
@qdot hi! Is there any chance we might take over the work from the original author? It has been quite a bit since they last showed interest, right? |
@stevekuznetsov I actually just started working on this exact thing! I've at least gotten their branch to build, but we'll need to bring it up to the 0.8 API, which I don't think should be too much work. They were doing this work in order to work with my other library, so my goal right now is to get that full chain built and working on a phone from the state it was in last year, then start updating the pieces from btleplug up. I'll let you know how this turns out. |
Good news! I've managed to get @gedgygedgy's full chain from btleplug up through buttplug running on my Pixel 3. This is going to require a TON of documentation, as said earlier, btleplug android (aka droidplug) expects to use its own async executor or thread registration, in order to keep the environment/thread contexts straight. Buttplug has an executor abstraction mechanism to handle this currently, but I'm... not real sure how to convey this requirement in relation to btleplug for normal users. The focus right now will be getting the old repo from btleplug 0.7 to btleplug 0.9, then I'll take a look at the executor context stuff for tokio. |
Wow that's super exciting! Please let me know when you get to a good spot if there's something I could help out with :) |
@qdot that's great news! What branch is this work happening on? I'd like to follow along. |
The work on bringing up android support to the 0.9.2 API is done and working, now I'm on to tokio runtime support, which is mostly there but hitting a some JNI bugs. Hopefully will get that ironed out in the next few days. I need to do some cleanup because I've been moving kinda fast and loose on this, but should have a branch up tomorrow. |
Ok, the android branch is now up as Unless you are well versed in JNI and poking at gradle by hand, I wouldn't recommend playing with this quite yet. In terms of how to build it:
#[no_mangle]
pub extern "C" fn JNI_OnLoad(vm: JavaVM, _res: *const c_void) -> jint {
let env = vm.get_env().unwrap();
android_utils::init(&env).unwrap();
jni_utils::init(&env).unwrap();
btleplug::platform::init(&env).unwrap();
jni::JNIVersion::V6.into()
}
So yeah, using this at the moment is a significant amount of work. I'm still stuck on tokio support right now. The find_class() calls to get the btleplug Peripheral java class fail when called on non-main threads, even if the thread is attached to the VM and has its classloader set to the main thread classloader. No idea what's up. I've got some vague solutions around caching ClassLoader calls during initialization that might do the trick but it's not a great solution. |
Ok, well, went ahead and implemented the ClassLoader cache and it makes the tokio multithreaded runtime work. That said, it's ugly and requires changes back through jni_utils. Would really like to figure out why env.find_class() isn't working but we have a backup solution if we absolutely need it. |
Finally did more cleanup and got the tokio code up. This requires:
THE android-utils-rs REPO IS NO LONGER NEEDED WHEN USING TOKIO AND THESE NEW BRANCHES So you should be able to:
Lemme know if you actually start trying this and either automate those steps in gradle (which I barely understand how to use) or need me to list out how I'm running things. (Next goal, which will happen fairly soon, is hopefully just kicking out an AAR) |
This is great progress @qdot ! Thank you so much! |
Hi, I am playing with this and actually managed to build a flutter app that starts without error. I'm trying the event_driven_discovery example inside a tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async { block. Unfortunately, line |
Ok, Android is now a supported platform in v0.10. Thanks to everyone who helped out on this! |
@trobanga Just curious, have you faced any issues with dead code removal when compiling flutter apps for release with btleplug? My flutter app works fine in debug, but crashes in release on android, in a way similar to what I was seeing when I'd had the wrong dependency for the droidplug.aar file that means it wasn't getting included. I'm wondering if I need to add some sort of explicit call into droidplug to keep it live or something, but that's just a guess at the moment. |
I just checked and in release mode my btleplugtest crashes immediately. Unfortunately I'm no Android expert and I don't have time to dig in at the moment. |
@trobanga I have a stopgap solution at #272 (comment) if you end up needing it. |
Hey! Great work on adding support for Android! I think I already know the answer but just confirming: is it possible to build a working CLI for Android? No UI, no flutter, no maven. Just a single binary (built with cross, for example) that I can call from Termux. Thanks! |
@denisidoro I... honestly have no idea. I'm not sure what kinda environment termux executes in, so I'm not sure if the JNI the process normally requires work would? |
I have followed all the steps above and everything else including the gist by @qdot and the steps in the main README.md file, but on Android I did not manage to get or discover any devices. I am able however to get the adapter which returns "Android" as the ID. I've noticed the following loop never works: let _ = adapter.start_scan(ScanFilter::default()).await;
while let Some(event) = events.next().await {
} And, getting the device lists this way returns an empty Vec: let peripherals = match adapter.peripherals().await {
} So, neither even-driven discovery nor getting the devices works for me. I'd appreciate any suggestion on what might be going wrong. Here is the relevant code: #[cfg(target_os = "android")]
#[allow(dead_code)]
pub static ANDROID_RUNTIME: once_cell::sync::OnceCell<tokio::runtime::Runtime> = once_cell::sync::OnceCell::new();
fn get_runtime() -> &'static tokio::runtime::Runtime {
#[cfg(not(target_os = "android"))]
return &RUNTIME;
#[cfg(target_os = "android")]
&ANDROID_RUNTIME.get().expect("Failed to get the Android runtime!")
}
#[cfg(target_os = "android")]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("JNI Error: {0}")]
Jni(#[from] jni::errors::Error),
#[error("Android class loader initialization has failed!")]
ClassLoader,
#[error("Tokio runtime initialization has failed!")]
Runtime,
#[allow(dead_code)]
#[error("Uninitialized Java VM!")]
JavaVM,
}
#[cfg(target_os = "android")]
#[allow(dead_code)]
static ANDROID_CLASS_LOADER: once_cell::sync::OnceCell<jni::objects::GlobalRef> = once_cell::sync::OnceCell::new();
#[cfg(target_os = "android")]
#[allow(dead_code)]
pub static ANDROID_JAVAVM: once_cell::sync::OnceCell<jni::JavaVM> = once_cell::sync::OnceCell::new();
#[cfg(target_os = "android")]
std::thread_local! {
static ANDROID_JNI_ENV: std::cell::RefCell<Option<jni::AttachGuard<'static>>> = std::cell::RefCell::new(None);
}
#[cfg(target_os = "android")]
#[allow(dead_code)]
fn android_setup_class_loader(env: &jni::JNIEnv) -> Result<(), Error> {
let thread = env
.call_static_method(
"java/lang/Thread",
"currentThread",
"()Ljava/lang/Thread;",
&[],
)?
.l()?;
let class_loader = env
.call_method(
thread,
"getContextClassLoader",
"()Ljava/lang/ClassLoader;",
&[],
)?
.l()?;
ANDROID_CLASS_LOADER
.set(env.new_global_ref(class_loader)?)
.map_err(|_| Error::ClassLoader)
}
#[cfg(target_os = "android")]
pub fn android_create_runtime() -> Result<(), Error> {
let vm = ANDROID_JAVAVM.get().ok_or(Error::JavaVM)?;
let env = vm.attach_current_thread().unwrap();
android_setup_class_loader(&env)?;
let runtime = {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_name_fn(|| {
static ATOMIC_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let id = ATOMIC_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
format!("intiface-thread-{}", id)
})
.on_thread_stop(move || {
ANDROID_JNI_ENV.with(|f| *f.borrow_mut() = None);
})
.on_thread_start(move || {
let vm = ANDROID_JAVAVM.get().unwrap();
let env = vm.attach_current_thread().unwrap();
let thread = env
.call_static_method(
"java/lang/Thread",
"currentThread",
"()Ljava/lang/Thread;",
&[],
)
.unwrap()
.l()
.unwrap();
env.call_method(
thread,
"setContextClassLoader",
"(Ljava/lang/ClassLoader;)V",
&[ANDROID_CLASS_LOADER.get().unwrap().as_obj().into()],
)
.unwrap();
ANDROID_JNI_ENV.with(|f| *f.borrow_mut() = Some(env));
})
.build()
.unwrap()
};
ANDROID_RUNTIME.set(runtime).map_err(|_| Error::Runtime)?;
Ok(())
}
#[cfg(target_os = "android")]
#[no_mangle]
pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, _res: *const std::os::raw::c_void) -> jni::sys::jint {
let env = vm.get_env().unwrap();
jni_utils::init(&env).unwrap();
btleplug::platform::init(&env).unwrap();
let _ = ANDROID_JAVAVM.set(vm);
jni::JNIVersion::V6.into()
} I initialize it like this: pub fn initialize() -> Result<(), Box<dyn std::error::Error + Send>> {
if is_initialized() {
return Err(Box::new(BleError::new(
"The BLE library has already been initialized!",
)));
}
#[cfg(target_os = "android")]
{
mylog!("Attempting BLE Android initialization...");
if let Err(error) = crate::android_create_runtime() {
return Err(Box::new(BleError::new(
format!("The BLE library initialization has failed: '{error:?}'").as_str(),
)));
}
mylog!("BLE Android initialization has succeeded!");
}
let adapters = get_runtime().block_on(get_adapters())?;
for adapter in &adapters {
get_runtime().block_on(start_scan(adapter))?;
}
Ok(())
} Update: Noticed I was ignoring the return value for the start_scan method, so I made the following changes and added a log to JNI_OnLoad to be sure it's getting fired: if let Err(error) = adapter.start_scan(ScanFilter::default()).await {
myerr!("Start scan failed: {error:?}");
}
#[cfg(target_os = "android")]
#[no_mangle]
pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, _res: *const std::os::raw::c_void) -> jni::sys::jint {
mylog!("Running JNI Onload...");
let env = vm.get_env().unwrap();
jni_utils::init(&env).unwrap();
btleplug::platform::init(&env).unwrap();
let _ = ANDROID_JAVAVM.set(vm);
jni::JNIVersion::V6.into()
} Now, I see the following outputs:
|
Hey, @qdot Thanks a lot for your library. You did an amazing work with android. Unfortunately, the documentation is a bit shallow and I lost a bit of time figuring out how to do it. So let me drop a few comments to help people without a good understanding of android / java. I managed to get the bluetooth working with tauri. You can check my code here. This is the first commit after I finally managed to get it working, so it is extremely dirty. I am expecting the code to be cleaner in a near future. There is a link to my discord in the readme if anyone need to reach me regarding my git repository. The few key points to get it working:
@NuLL3rr0r In your update, it looks like you forgot to call start_scan in the "java" thread. |
Yeah, I'm holding off on writing full documentation about compiling for mobile until I know the system actually works everywhere. I'm shipping this in an app with ~10k installs at the moment and we're still seeing a steady stream of very weird, untraceable crashes. Not only that, sometimes the app won't crash but also just won't connect to anything (a big problem on Samsung phones, for some reason). In terms of the instructions you provided (looks like you grabbed the init code from the rust bridge in https://github.com/intiface/intiface-central )? , do note that this method will work for keeping the app focused, but scanning and continuing connections while foregrounding may require extra permissions. |
And here's what your permissions should ideally look like, as you'll want to restrict them based on the Android version your app is running on: https://github.com/intiface/intiface-central/blob/main/android/app/src/main/AndroidManifest.xml |
Thanks for the hints! I will update everything. It is mostly based on the following codes but yeah I looked a bit around at your code: |
Ah, ok, yeah, here's the current version of that gist, which I adapted off of trobanga's original shim layer: https://github.com/intiface/intiface-central/blob/main/intiface-engine-flutter-bridge/src/mobile_init/setup/android.rs |
@trobanga Oh nice, thanks! I'd been waiting on frb v2 to go stable first, this'll be a nice guide once that's ready. Looks nice and clean! |
Has there been any follow-up on a guide for getting this working? |
Hi everyone, I'm creating a Tauri plugin to use thermal printers. The plugin handles all permissions (ask and check) to use BLE. The crate btleplug is wrapped in a crate called eco_print. It works on desktops, but I'm having trouble making it work on mobile (Android). I'm trying to find a way to build the crate for use in my plugin, tauri-plugin-escpos. However, I'm not sure about the exact steps needed to build the crate for Android. Any guidance on this would be greatly appreciated. Thank you! |
Hi @lnxdxtf I would recommend taking a look at my repo. I have a tauri app with bluetooth for an indoor bike: https://gitlab.com/loikki/open-biking |
For anyone that gets btleplug running on android, please let me know! I'll try to remember to add project listings to the readme so other people can use them as examples. Posting project links here is fine. |
I am trying to get this to work as as CLI app built using cross, just like @denisidoro. |
I don't think Termux requests Bluetooth permissions, so this would require a proxy APK which in turn would use the Termux API. But I may be wrong |
Hi, I followed your approach, but in mobile.rs, the line btleplug::platform::init(&env).unwrap(); results in an error saying that the init function cannot be found. Can you explain why? Thank you! |
Can you send the logs please? Without them it is hard to understand what is going on. I am a bit rusty on the bluetooth side, so I might be asking the wrong questions. Is it during compilation or running? If during compilation:
else:
|
thank you, I have implemented it according to your instructions, and it is now working on Android. |
Hi @loikki @qdot , I am building a flutter app connected to a Rust backend using btleplug, similar to your Tauri app you gave as an example. I am now able to scan, but rust panics when I try to connect to the ble device I want to connect to. This is the error:
Would you know why? A similar problem occured for @lnxdxtf in #395 but the answer was never given. This is my rust code to interface ble with android. Excuses in advance for my rust, I am learning at the same time. I can give more info if needed.
|
I'm not sure why people keep replying to a closed issue instead of filing new bugs, but I'm gonna go ahead and lock this issue. Please file new issues for things that are, you know, new issues. |
Same idea as blurmac. Fork bluedroid, bring in code, fit to whatever our API surface is.
The text was updated successfully, but these errors were encountered: