#ble #gatt #bluetooth #android #bluest

android-ble

Android Bluetooth API wrapper, currently supporting BLE client role operations

2 releases

0.1.1 Feb 18, 2026
0.1.0 Feb 6, 2026

#1080 in Hardware support

BSD-2-Clause OR Apache-2.0

2MB
33K SLoC

android-ble-rs

Android Bluetooth API wrapper, currently supporting BLE client role operations.

A few portions of the code (especially L2capChannel) is orginally written by Dirbaio.

Version 0.1.x of this crate is supposed to be API-compatible with version 0.6.x of the bluest library. In fact, some type definitions are copied from bluest. Anything incompatible with bluest in the API may be reported as a bug.

This crate uses ndk_context, which is automatically initialized in android_activity.

Test

Make sure the Android SDK, NDK, Rust target aarch64-linux-android and cargo-apk are installed. Note that cargo-apk2 can also be used.

Create android-ble-test according to the template provided below, and build it with cargo apk build -r. Note: -r means building for the release profile, which produces a much smaller package.

Install the target/release/apk/android-ble-test.apk on the Android device, and enable permissions manually on the device.

Start the android-ble-test on the device, then check the log output with adb logcat android_ble_test:D '*:S'.

cargo-apk template

Cargo.toml:

[package]
name = "android-ble-test"
version = "0.1.0"
edition = "2024"
publish = false

[dependencies]
log = "0.4"
android-ble = "0.1"
android_logger = "0.15.1"
ndk-context = "0.1.1"
android-activity = { version = "0.6", features = ["native-activity"] }
# jni-min-helper = { version = "0.3", features = ["futures"] }
futures-lite = "2.6"
async-channel = "2.2.0"

[lib]
crate-type = ["cdylib"]

[package.metadata.android]
package = "com.example.android_ble_test"

build_targets = ["aarch64-linux-android"]

# For `cargo-apk2`:
# put <https://docs.rs/crate/jni-min-helper/0.3.2/source/java/PermActivity.java> in this folder
# java_sources = "java"

# Android 12 or above may require runtime permission request. Use `cargo-apk2` for performing this.
# <https://developer.android.com/develop/connectivity/bluetooth/bt-permissions>
# <https://docs.rs/jni-min-helper/0.3.2/jni_min_helper/struct.PermissionRequest.html>
[package.metadata.android.sdk]
min_sdk_version = 23
target_sdk_version = 33

[[package.metadata.android.uses_feature]]
name = "android.hardware.bluetooth_le"
required = true

[[package.metadata.android.uses_permission]]
name = "android.permission.BLUETOOTH_SCAN"
min_sdk_version = 31
# TODO: add `usesPermissionFlags` (neverForLocation) when it becomes supported in `cargo-apk2`.

[[package.metadata.android.uses_permission]]
name = "android.permission.BLUETOOTH_CONNECT"
min_sdk_version = 31

[[package.metadata.android.uses_permission]]
name = "android.permission.ACCESS_FINE_LOCATION"
# TODO: uncomment this line when `usesPermissionFlags` becomes supported in `cargo-apk2`.
# max_sdk_version = 30

[[package.metadata.android.uses_permission]]
name = "android.permission.BLUETOOTH"
max_sdk_version = 30

[[package.metadata.android.uses_permission]]
name = "android.permission.BLUETOOTH_ADMIN"
max_sdk_version = 30

# configurations below are for `cargo-apk2`

# [[package.metadata.android.application.activity]]
# name = "android.app.NativeActivity"

# [[package.metadata.android.application.activity.intent_filter]]
# actions = ["android.intent.action.VIEW", "android.intent.action.MAIN"]
# categories = ["android.intent.category.LAUNCHER"]

# [[package.metadata.android.application.activity.meta_data]]
# name = "android.app.lib_name"
# value = "android_ble_test"

# [[package.metadata.android.application.activity]]
# name = "rust.jniminhelper.PermActivity"

src/lib.rs:

use android_ble as bluest;

use android_activity::{AndroidApp, MainEvent, PollEvent};
use futures_lite::{FutureExt, StreamExt};
use log::info;

#[unsafe(no_mangle)]
fn android_main(app: AndroidApp) {
    // View tracing log on the host PC with `adb logcat android_ble_test:D '*:S'`.
    android_logger::init_once(
        android_logger::Config::default()
            .with_max_level(log::LevelFilter::Info)
            .with_tag("android_ble_test".as_bytes()),
    );

    // calling `block_on` with bluetooth operations in `android_main` thread may block forever.
    let (tx, rx) = async_channel::unbounded();
    std::thread::spawn(move || {
        let res = futures_lite::future::block_on(async_main().or(async {
            let _ = rx.recv().await;
            info!("async thread received stop signal.");
            Ok(())
        }));
        if let Err(e) = res {
            info!("async thread's `block_on` received error: {e}");
        } else {
            info!("async thread terminates itself.");
        }
    });

    let mut on_destroy = false;
    loop {
        app.poll_events(None, |event| match event {
            PollEvent::Main(MainEvent::Stop) => {
                info!("Main Stop Event.");
                let _ = tx.send(());
            }
            PollEvent::Main(MainEvent::Destroy) => {
                on_destroy = true;
            }
            _ => (),
        });
        if on_destroy {
            return;
        }
    }
}

// Please put your new test case here.
async fn async_main() -> Result<(), Box<dyn std::error::Error>> {
    // Currently this requires `cargo-apk2` instead of `cargo-apk` to work.
    // But this is required if the user chooses to confirm permission on every startup.
    /*
    let perm_list = if jni_min_helper::android_api_level() >= 31 {
        vec![
            "android.permission.BLUETOOTH_SCAN",
            "android.permission.BLUETOOTH_CONNECT",
            "android.permission.ACCESS_FINE_LOCATION", // TODO: remove this
        ]
    } else {
        vec![
            "android.permission.BLUETOOTH",
            "android.permission.BLUETOOTH_ADMIN",
            "android.permission.ACCESS_FINE_LOCATION",
        ]
    };
    let req = jni_min_helper::PermissionRequest::request("BLE Test", perm_list)?;
    if let Some(req) = req {
        info!("requesting permissions...");
        let result = req.await;
        for (perm_name, granted) in result.unwrap_or_default() {
            if !granted {
                info!("{perm_name} is denied by the user.");
                return Ok(());
            }
        }
    };
    */
    
    let adapter = bluest::Adapter::default().await.ok_or("adapter is unavailable")?;
    adapter.wait_available().await?;
    info!("starting scan...");
    let mut scan = adapter.scan(&[]).await?;
    info!("scan started.");
    while let Some(discovered) = scan.next().await {
        info!("found a device...");
        info!("{:?}", discovered);
    }
    Ok(())
}

Dependencies

~1.4–3MB
~42K SLoC