Usage
Build and use SimpleDroidBLE in Android/Kotlin applications.
SimpleDroidBLE is the Android/Kotlin wrapper around the SimpleBLE Android backend. Use it when your application is a normal Android app and you want Kotlin APIs, Android runtime permission helpers, coroutine-friendly operations, and flows for scan, connection, and notification events.
If you are already writing native C++ inside an Android app, use SimpleBLE directly and include simpledroidbridge. SimpleDroidBLE is for Android-first app code.
Requirements
- Android API 31 or newer.
- Android Studio.
- Android NDK r29 when building SimpleDroidBLE from source.
- Bluetooth permissions declared in
AndroidManifest.xmland requested at runtime.
Install from Maven Central
Add SimpleDroidBLE to your app module:
dependencies {
implementation("org.simpleble:simpledroidble:<version>")
}The artifact includes the native libraries for arm64-v8a, armeabi-v7a, x86, and x86_64. Gradle resolves
SimpleDroidBridge, AndroidX Core, and Kotlin coroutines transitively. Make sure AndroidX is enabled in
gradle.properties:
android.useAndroidX=trueConsuming Locally
From a local checkout, use a Gradle composite build before include(":app"):
includeBuild("path/to/simpleble/simpledroidble") {
dependencySubstitution {
substitute(module("org.simpleble:simpledroidble")).using(project(":simpledroidble"))
}
}
include(":app")Then add the dependency:
dependencies {
implementation("org.simpleble:simpledroidble:<version>")
}Consumer builds always use the Android Bluetooth backend. The PLAIN backend is available only as a repository test build.
Android Permissions
Declare Android 12+ Bluetooth permissions in your app manifest:
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />If your app uses BLE scans to infer physical location, review Android location policy before using neverForLocation.
Request the runtime permissions before calling adapter APIs. The permission helpers are stateless; the native library loads automatically on the first adapter call.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!SimpleDroidBle.hasPermissions(this)) {
SimpleDroidBle.requestPermissions(this)
return
}
// Safe to create adapters after permissions are available.
}
}In modern Android apps, you can also use ActivityResultContracts.RequestMultiplePermissions() with SimpleDroidBle.requiredPermissions. Peripheral-only applications should use requiredPeripheralPermissions, hasPeripheralPermissions(), and requestPeripheralPermissions() so they do not request scan access. Applications that support both roles can combine the two permission arrays and remove duplicates.
Quickstart
SimpleDroidBLE exposes scan and connection events as flows. Operations that wait for Android Bluetooth callbacks are suspending and safe to call from the main thread:
val adapter = Adapter.getAdapters().first()
val scanJob = lifecycleScope.launch {
adapter.onScanFound.collect { peripheral ->
Log.d("SimpleBLE", "Found ${peripheral.identifier} [${peripheral.address}]")
}
}
lifecycleScope.launch {
adapter.scanFor(5_000)
scanJob.cancelAndJoin()
}Connect and inspect GATT services:
val peripheral = adapter.scanGetResults().first { it.isConnectable }
peripheral.connect()
val services = peripheral.services()
services.forEach { service ->
service.characteristics.forEach { characteristic ->
Log.d("SimpleBLE", "${service.uuid} / ${characteristic.uuid}")
}
}Service, characteristic, and descriptor UUIDs are BluetoothUUID values and can be passed directly to GATT operations. Bluetooth UUIDs and addresses expose their underlying string through value when needed.
Read, write, and subscribe:
val service = BluetoothUUID("0000180f-0000-1000-8000-00805f9b34fb")
val characteristic = BluetoothUUID("00002a19-0000-1000-8000-00805f9b34fb")
val value = peripheral.read(service, characteristic)
peripheral.writeRequest(service, characteristic, byteArrayOf(0x01, 0x02))
val notifyJob = lifecycleScope.launch {
peripheral.notify(service, characteristic).collect { payload ->
Log.d("SimpleBLE", "Notification bytes: ${payload.size}")
}
}
notifyJob.cancelAndJoin() // Waits for automatic unsubscribe.
peripheral.disconnect()Use writeRequest for acknowledged writes and writeCommand for write-without-response characteristics. Descriptor read and write overloads are also available.
Host a peripheral
Create the peripheral from an adapter and pass any Android Context; SimpleDroidBLE retains only its application context. Configure the complete GATT table before start():
val adapter = Adapter.getAdapters().first()
val peripheral = adapter.createLocalPeripheral(applicationContext)
peripheral.addAdvertisedService(
BluetoothUUID("12345678-1234-5678-1234-56789abcdef0")
)
val characteristic = peripheral
.addService(BluetoothUUID("12345678-1234-5678-1234-56789abcdef0"))
.addCharacteristic(
BluetoothUUID("12345678-1234-5678-1234-56789abcdef1"),
LocalCharacteristicCapability.Read,
LocalCharacteristicCapability.WriteRequest,
LocalCharacteristicCapability.WriteCommand,
LocalCharacteristicCapability.Notify,
LocalCharacteristicCapability.Indicate
)
characteristic.value = "ready".encodeToByteArray()
characteristic.setWriteHandler { value ->
characteristic.value = value // Store and publish the echo to subscribers.
}
lifecycleScope.launch {
peripheral.start()
}setWriteHandler receives every accepted write on SimpleDroidBLE's callback thread and is the reliable place for application behavior. Keep it short. onWrite is a bounded hot flow intended for UI and telemetry-style observation. setReadHandler provides a dynamic value; without it, reads return value directly.
Assigning value publishes the new bytes to subscribed clients when the characteristic supports notify or indicate. onSubscribed and onUnsubscribed expose subscription transitions, while onClientConnected and onClientDisconnected expose Android's GATT server connection events.
Call stop() from your lifecycle cleanup. Advertising data, services, and characteristics cannot be changed while started; values and handlers can.
SimpleDroidBLE includes the phone's system Bluetooth name. Android's public advertising API cannot set an arbitrary per-advertisement name.
Peripheral mode currently uses legacy advertising, including its 31-byte payload limits, and rejects prepared writes. Use regular write requests or write commands within the negotiated GATT payload size.
Lifecycle And Threading
- Create adapters only after runtime permissions have been granted.
- Stop active scans from
onPause()or an equivalent lifecycle callback. - Keep notification collection in a lifecycle-aware coroutine. Cancelling the
notifyorindicateflow waits for native unsubscribe before collection finishes. - Stop a local peripheral when its owning lifecycle ends.
start()andstop()dispatch blocking Bluetooth setup and teardown toDispatchers.IO. - Only one
notifyorindicatecollector may be active for a characteristic. Cancel and join the old collector before starting another. - Notification and indication flows use bounded buffers and fail with
SimpleDroidBleExceptionif a collector cannot keep up. Placebuffer(256)directly afternotifyorindicateto increase capacity, or explicitly opt into dropping withbuffer(256, onBufferOverflow = BufferOverflow.DROP_OLDEST)orBufferOverflow.DROP_LATEST. - Flow collectors resume in their own coroutine context. Keep payload parsing short; blocking GATT operations dispatch to
Dispatchers.IOinternally. - Android exposes one active Bluetooth adapter through the backend today.
Adapter.getAdapters()returns a list for API consistency.
Error Handling
Native SimpleBLE operation failures are surfaced as SimpleDroidBleException, an unchecked Kotlin/Java exception. Wrap scan, connect, read, write, notify, unsubscribe, and disconnect operations in normal Kotlin try/catch blocks and show errors to users. Bluetooth can fail because permissions are missing, Bluetooth is off, a device disappeared, a GATT operation timed out, or a characteristic does not support the requested operation.
Current Limitations
- The JNI layer still keeps process-local native caches for adapters, peripherals, and active notification callbacks. Treat adapter and peripheral objects as app-process objects, not serializable handles.
- Reconnection works through the backend after disconnect, but apps should refresh services after each new connection.
- Android address type is reported as unspecified on API levels where Android does not expose it.
- Programmatic unpairing is intentionally not relied on because Android restricts bond removal.
