Skip to content
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

Closed
qdot opened this issue Jan 11, 2020 · 62 comments
Closed

Add Android Support #8

qdot opened this issue Jan 11, 2020 · 62 comments
Labels
android Issues related to the android core

Comments

@qdot
Copy link
Contributor

qdot commented Jan 11, 2020

Same idea as blurmac. Fork bluedroid, bring in code, fit to whatever our API surface is.

@qdot qdot added the android Issues related to the android core label Jan 11, 2020
@mvniekerk
Copy link

Hi - I see this sits on Jan 11. How can I help?

@kitlith
Copy link

kitlith commented Mar 26, 2021

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.

@gedgygedgy
Copy link
Contributor

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:

  1. As mentioned above, it has a C middleware layer which can and should be stripped out and replaced with pure Rust.
  2. It is asynchronous in nature, due to Android's asynchronous Bluetooth API. This needs to be made synchronous.
  3. And of course, it does not match the btleplug API whatsoever.

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.

@qdot
Copy link
Contributor Author

qdot commented Jun 4, 2021

@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.

@gedgygedgy
Copy link
Contributor

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 JavaVM, which unfortunately can't be obtained from JNI_CreateJavaVM() or JNI_GetCreatedJavaVMs(). It can only be obtained by implementing JNI_OnLoad() in your library or as a parameter to a JNI function. This makes it impossible to implement Java support without either:

  1. Adding parameters to Manager::new() or
  2. Creating a global variable (ick!) which is initialized by JNI_OnLoad() (or some init function which gets called by another library), similar to how blurdroid does it.

Manager::new() is not currently bound by a trait, which makes it tempting to simply add a JavaVM argument to Manager::new(), but this would make things slightly more difficult for Android clients, which now have to conditionally (by #[cfg]) pass a JavaVM around in their Bluetooth code.

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 JNI_OnLoad(), and makes Rust angry by creating global variables.

Neither option is very appealing. What are your thoughts? Any other ideas on how to make this work?

@gedgygedgy
Copy link
Contributor

gedgygedgy commented Jun 8, 2021

Personally, I would prefer to add a parameter to Manager::new(). The Manager will (hopefully) be created early on, at a high level in the program, where it will be easier to conditionally pass a JavaVM around, and there will likely only be one Manager.

@kitlith
Copy link

kitlith commented Jun 9, 2021

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 #[cfg]s necessary. Expose a function to set the JavaVM for the library, use a OnceCell or similar to store it, call it a day. I will note that said function does not need to be called in JNI_OnLoad (though it is convienent), just sometime before someone calls Manager::new().

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.

@gedgygedgy
Copy link
Contributor

After seeing how easy OnceCell is to use (doesn't even require unsafe), I am now thoroughly convinced that a blteplug::platform::init() approach is the right way to go.

I've started working on the Android port. You can check out my progress here.

@gedgygedgy
Copy link
Contributor

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.

@gedgygedgy
Copy link
Contributor

gedgygedgy commented Jun 18, 2021

What is btleplug's expectation for attempting to run two async commands at the same time? For example, one thread calls Peripheral::connect(), and then another thread calls Peripheral::disconnect() before Peripheral::connect() is finished. Is btleplug supposed to handle this, or is the user just supposed to not do this? Android's Bluetooth system doesn't seem to handle simultaneous commands very well, so I'm wondering if I need to set up some sort of command queue.

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.

@qdot
Copy link
Contributor Author

qdot commented Jun 18, 2021

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. :)

@gedgygedgy
Copy link
Contributor

I have a working Peripheral::connect() and Peripheral::disconnect() implementation. It's a bit of a proof-of-concept and needs to be cleaned up with a proper command queue, but it works.

@qdot
Copy link
Contributor Author

qdot commented Jun 24, 2021

I have a working Peripheral::connect() and Peripheral::disconnect() implementation. It's a bit of a proof-of-concept and needs to be cleaned up with a proper command queue, but it works.

WOOOOOOOOOOOOOOOOOOOO 👍🏻 💯 🥇 🚀

@gedgygedgy
Copy link
Contributor

gedgygedgy commented Jun 25, 2021

I've added a command queue to ensure commands execute one at a time. Peripheral::discover_characteristics(), Peripheral::read(), and Peripheral::write() are all working. The rest of the functionality is on its way.

@gedgygedgy
Copy link
Contributor

At this point I think I've implemented all of the functionality for Android. Scanning, CentralEvents, reading/writing characteristics, peripheral properties, and characteristic notifications are all working.

A few caveats:

  • This is somewhat dependent on my API changes in Various API updates #166.
  • This is also dependent on jni-utils-rs, which has not yet stabilized, and which I have not yet published to crates.io.
  • Android apps that use this functionality must declare a runtime-only dependency on the .aar file generated by src/droidplug/java (which itself also depends on the Java code from jni-utils-rs.)
  • This code interacts heavily with the JVM. As such, any thread that interacts with btleplug must be attached to the JVM. If the thread is created natively (rather than by a java.lang.Thread), it must call JavaVM::attach_current_thread(), and it must also set the thread's classloader. While this isn't a dealbreaker for systems like tokio, it does make them more difficult to use.
  • For some reason, not all events that get sent by AdapterManager::emit() seem to go through, at least on my setup. (Strangely, adding print statements between the emit() calls seems to help somewhat. Perhaps there's a race condition here?)
  • Some apps won't want to use Adapter::start_scan() and Adapter::stop_scan(), because this requires location permissions on Android. To allow such apps to use the Companion Device Manager instead, I've provided an Adapter::report_scan_result() function, which takes an android.bluetooth.le.ScanResult and applies it as if it had been discovered by its own scan. In addition, clients can use Adapter::add_peripheral() to skip the scanning step if they already know the MAC address (probably retrieved from the Companion Device Manager.)
  • I have not written any documentation yet. That is going to be my next step.

@kitlith
Copy link

kitlith commented Jul 2, 2021

This code interacts heavily with the JVM. As such, any thread that interacts with btleplug must be attached to the JVM. If the thread is created natively (rather than by a java.lang.Thread), it must call JavaVM::attach_current_thread(), and it must also set the thread's classloader. While this isn't a dealbreaker for systems like tokio, it does make them more difficult to use.

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

@schell
Copy link

schell commented Jan 1, 2022

What's the status on this? Did any of these changes make it into 0.9?

@qdot
Copy link
Contributor Author

qdot commented Jan 1, 2022

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.

@stevekuznetsov
Copy link

@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?

@qdot
Copy link
Contributor Author

qdot commented Apr 16, 2022

@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.

@qdot
Copy link
Contributor Author

qdot commented Apr 18, 2022

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.

@stevekuznetsov @schell

@stevekuznetsov
Copy link

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 :)

@schell
Copy link

schell commented Apr 21, 2022

@qdot that's great news! What branch is this work happening on? I'd like to follow along.

@qdot
Copy link
Contributor Author

qdot commented Apr 21, 2022

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.

@schell @stevekuznetsov

@qdot
Copy link
Contributor Author

qdot commented Apr 23, 2022

Ok, the android branch is now up as android-update on this repo. I'll open a PR on it also, just so others will know, but I have a feeling it may be a while before it comes in.

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:

  • You'll need to clone gedgygedgy/jni-rs, gedgegedgy/jni-utils-rs, and gedgygedgy/android-utils-rs and have them next to this repo in the file system. I currently just set up local path dependencies in cargo between all of them.
  • You'll write/build your rust code as normal, I recommend using cargo ndk.
  • You'll need to local path links from your app gradle to this library as well as jni-rs probably. You'll also need to call init on all the libraries on JNI_OnLoad, it looks something like
#[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.

@qdot
Copy link
Contributor Author

qdot commented Apr 23, 2022

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.

@qdot
Copy link
Contributor Author

qdot commented Apr 24, 2022

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:

  • Either build java directories or add them to your app gradle as local path dependencies
  • Set the btleplug jni-utils-rs dep to your local checkout on the classcache branch (I think I have the cargo file already doing this and assuming the repo directories are next to each other)
  • Build btleplug using cargo ndk
  • Copy those output files to the proper native lib architecture directory in your app.

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)

@schell
Copy link

schell commented Apr 25, 2022

This is great progress @qdot ! Thank you so much!

@trobanga
Copy link
Contributor

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 central.start_scan(ScanFilter::default()).await.unwrap(); fails with JniCall(ThreadDetached). Do you have any ideas what's happening there?

@qdot
Copy link
Contributor Author

qdot commented Jul 30, 2022

Ok, Android is now a supported platform in v0.10. Thanks to everyone who helped out on this!

@qdot qdot closed this as completed Jul 30, 2022
@qdot
Copy link
Contributor Author

qdot commented Oct 21, 2022

@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.

@trobanga
Copy link
Contributor

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.

@qdot
Copy link
Contributor Author

qdot commented Oct 22, 2022

@trobanga Ok cool, thanks for checking that! I just filed #272, and will be working on this myself. Pretty sure I just need to add some sort of explicit access to the aar to keep the file alive.

@qdot
Copy link
Contributor Author

qdot commented Oct 22, 2022

@trobanga I have a stopgap solution at #272 (comment) if you end up needing it.

@denisidoro
Copy link

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!

@qdot
Copy link
Contributor Author

qdot commented Jan 7, 2023

@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?

@NuLL3rr0r
Copy link
Contributor

NuLL3rr0r commented Dec 7, 2023

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:

Running JNI Onload...
Start scan failed: Other(JniCall(ThreadDetached))

@loikki
Copy link

loikki commented Feb 13, 2024

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:

  • Add permissions to your application (I might be asking too much here): AndroidManifest.xml
  • Ask for permissions in your application: MainActivity.kt
  • Initialize btleplug in "java": JNI_Onload
  • Add the java code from both jni-utils-rs and btleplug to your android application: Makefile
  • Start a "java" thread for managing the bluetooth: create_runtime
  • Only ever access the bluetooth interface from within the thread: scan

@NuLL3rr0r In your update, it looks like you forgot to call start_scan in the "java" thread.

@qdot
Copy link
Contributor Author

qdot commented Feb 13, 2024

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.

@qdot
Copy link
Contributor Author

qdot commented Feb 13, 2024

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

@loikki
Copy link

loikki commented Feb 14, 2024

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:

@qdot
Copy link
Contributor Author

qdot commented Feb 14, 2024

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
Copy link
Contributor

@qdot interesting to see that you are using flutter_rust_bridge, too. Have a look at frb2 if you are interested in an adaption to flutter_rust_bridge v2.

@qdot
Copy link
Contributor Author

qdot commented Feb 15, 2024

@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!

@Gibbz
Copy link

Gibbz commented Jul 26, 2024

Has there been any follow-up on a guide for getting this working?
I'm about to start the process myself. And the info seems fragmented... See how I go!
@qdot

@lnxdxtf
Copy link

lnxdxtf commented Jul 29, 2024

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!

@loikki
Copy link

loikki commented Jul 29, 2024

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
The modifications to the android files are all located in android-files and the commands for building / running are in my Makefile. If you have any question, feel free to open an issue in my repo

@qdot
Copy link
Contributor Author

qdot commented Jul 29, 2024

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.

@TheCataliasTNT2k
Copy link

I am trying to get this to work as as CLI app built using cross, just like @denisidoro.
Any instructions, how to do this?

@denisidoro
Copy link

denisidoro commented Nov 12, 2024

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

@zhenzhongli
Copy link

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:

  • Add permissions to your application (I might be asking too much here): AndroidManifest.xml
  • Ask for permissions in your application: MainActivity.kt
  • Initialize btleplug in "java": JNI_Onload
  • Add the java code from both jni-utils-rs and btleplug to your android application: Makefile
  • Start a "java" thread for managing the bluetooth: create_runtime
  • Only ever access the bluetooth interface from within the thread: scan

@NuLL3rr0r In your update, it looks like you forgot to call start_scan in the "java" thread.

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!

@loikki
Copy link

loikki commented Dec 10, 2024

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:

  • Are you using the same version of the library than me?

else:

  • Is the java environment well set when you call the function?
  • On the right thread?
  • Do you have the required permission for bluetooth?

@zhenzhongli
Copy link

zhenzhongli commented Dec 11, 2024

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:

  • Are you using the same version of the library than me?

else:

  • Is the java environment well set when you call the function?
  • On the right thread?
  • Do you have the required permission for bluetooth?

thank you, I have implemented it according to your instructions, and it is now working on Android.

@XavierIsabel
Copy link

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:

I/flutter (29654): panicked at C:\Users\xavie\.cargo\registry\src\index.crates.io-6f17d22bba15001f\jni-utils-0.1.1\rust\future.rs:42:98:
I/flutter (29654): called `Option::unwrap()` on a `None` value

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.

#[cfg(target_os = "android")]
use std::sync::Once;
#[cfg(target_os = "android")]
use once_cell::sync::OnceCell;

#[cfg(target_os = "android")]
pub static JAVA_VM: OnceCell<JavaVM> = OnceCell::new();

#[cfg(target_os = "android")]
use jni::{JNIEnv, JavaVM};
#[cfg(target_os = "android")]
use jni::objects::{JClass, GlobalRef};
#[cfg(target_os = "android")]
use jni::errors::Error;

#[cfg(target_os = "android")]
static INIT: Once = Once::new();

#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_com_example_sifi_1gui_RustLibBLE_initializeJavaVM(env: JNIEnv, _: JClass) {
    let java_vm = env.get_java_vm().expect("Failed to get JavaVM");

    JAVA_VM.set(java_vm);

    INIT.call_once(|| {
        btleplug::platform::init(&env);
    });
}

#[cfg(target_os = "android")]
fn get_java_vm() -> &'static JavaVM {
    JAVA_VM.get().expect("JavaVM is not initialized")
}

#[cfg(target_os = "android")]
pub fn attach_thread_to_jvm() -> Result<JNIEnv<'static>, Error> {
    let java_vm = get_java_vm();
    java_vm.attach_current_thread_as_daemon()
}

#[cfg(target_os = "android")]
pub fn detach_thread_from_jvm() {
    let java_vm = get_java_vm();
    java_vm.detach_current_thread();
}

#[cfg(target_os = "android")]
pub fn setup_class_loader(env: &JNIEnv) -> Result<GlobalRef, 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()?;

    Ok(env.new_global_ref(class_loader)?)
}

@qdot
Copy link
Contributor Author

qdot commented Dec 23, 2024

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.

@deviceplug deviceplug locked as resolved and limited conversation to collaborators Dec 23, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
android Issues related to the android core
Projects
None yet
Development

No branches or pull requests