Android: Connect via Bluetooth
This guide shows how to integrate @onekeyfe/hd-common-connect-sdk in a native Android host via a low-level adapter. The JavaScript bundle runs in a WebView; transport calls are forwarded to native (Nordic BLE) and bridged back to JS.
Key libraries:
WebView JS bridge:
com.smallbuer:jsbridge:1.0.7Nordic BLE (Kotlin, ≥ 1.1.0):
no.nordicsemi.android.kotlin.ble:scanner:1.1.0no.nordicsemi.android.kotlin.ble:client:1.1.0Reason:
includeStoredBondedDevicesinBleScannerSettingsrequires > 1.0.9
OneKey BLE UUIDs:
Service:
00000001-0000-1000-8000-00805f9b34fbWrite:
00000002-0000-1000-8000-00805f9b34fbNotify:
00000003-0000-1000-8000-00805f9b34fb
Step 1. Gradle and Manifest
Gradle (app/build.gradle.kts):
dependencies {
implementation("com.smallbuer:jsbridge:1.0.7")
implementation("no.nordicsemi.android.kotlin.ble:scanner:1.1.0")
implementation("no.nordicsemi.android.kotlin.ble:client:1.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}Android 12+ permissions (AndroidManifest.xml):
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<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_SCAN" android:usesPermissionFlags="neverForLocation"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>Runtime permissions (Kotlin):
private fun ensureBluetoothPermissions(): Boolean {
val needs = mutableListOf<String>()
if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) needs += android.Manifest.permission.BLUETOOTH_SCAN
if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) needs += android.Manifest.permission.BLUETOOTH_CONNECT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) needs += android.Manifest.permission.ACCESS_FINE_LOCATION
if (needs.isNotEmpty()) {
ActivityCompat.requestPermissions(this, needs.toTypedArray(), 1001)
return false
}
return true
}Step 2. Build the web bundle (already implemented in the demo)
Inside the hardware-js-sdk repo:
cd packages/connect-examples/native-android-example/web
yarn && yarn build # emits web/web_dist/Copy the entire output folder into your Android project:
Copy
web/web_dist/→app/src/main/assets/web_dist/The entry file will be available as:
app/src/main/assets/web_dist/index.html
No further JS work is required in the native app; the demo’s web bundle already initializes env: 'lowlevel' and wires the low-level adapter.
Step 3. WebView + Bridge (register before loading)
class MainActivity : AppCompatActivity() {
lateinit var webview: BridgeWebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webview = findViewById(R.id.webview)
configureWebView()
// Register native handlers BEFORE loading html
registerHandlers()
// Load built HTML bundle copied into assets/web_dist/
webview.loadUrl("file:///android_asset/web_dist/index.html")
}
private fun configureWebView() {
val s = webview.settings
s.javaScriptEnabled = true
s.domStorageEnabled = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) s.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
}
}Step 4. BLE scan (include stored bonded devices)
private val ONEKEY_SERVICE_UUID: UUID = UUID.fromString("00000001-0000-1000-8000-00805f9b34fb")
private val bleScanner by lazy { BleScanner(this) }
private val aggregator = BleScanResultAggregator()
data class OneKeyDeviceInfo(val id: String, val name: String)
private fun startScan(onResult: (List<OneKeyDeviceInfo>) -> Unit) {
if (!ensureBluetoothPermissions()) return
val settings = BleScannerSettings(
scanMode = BleScanMode.LOW_LATENCY,
filter = BleScanFilter(serviceUUIDs = listOf(FilteredServiceUuid(ONEKEY_SERVICE_UUID))),
includeStoredBondedDevices = true, // requires Nordic BLE >= 1.1.0
)
aggregator.reset()
bleScanner.scan(settings)
.map { aggregator.aggregate(it) }
.onEach { results ->
val devices = results.mapNotNull { r -> r.device }
.distinctBy { it.address }
.map { OneKeyDeviceInfo(id = it.address, name = it.name ?: "") }
onResult(devices)
}
.launchIn(lifecycleScope)
}Bridge handler for enumerate:
private fun registerHandlers() {
webview.registerHandler("enumerate", BridgeHandler { _, cb ->
startScan { list -> cb.onCallBack(Gson().toJson(list)) }
})
// The rest of handlers are shown below
}Step 5. Connect, characteristics, notifications
private var connection: ClientBleGatt? = null
private var writeCh: ClientBleGattCharacteristic? = null
private var notifyCh: ClientBleGattCharacteristic? = null
webview.registerHandler("connect", BridgeHandler { data, cb ->
if (!ensureBluetoothPermissions()) return@BridgeHandler
val json = JsonParser.parseString(data).asJsonObject
val mac = json.get("uuid").asString
lifecycleScope.launch(Dispatchers.IO) {
val gatt = ClientBleGatt.getInstance(this@MainActivity, mac)
connection = gatt
writeCh = gatt.getCharacteristic(ONEKEY_SERVICE_UUID, UUID.fromString("00000002-0000-1000-8000-00805f9b34fb"))
notifyCh = gatt.getCharacteristic(ONEKEY_SERVICE_UUID, UUID.fromString("00000003-0000-1000-8000-00805f9b34fb"))
notifyCh?.getNotifications()?.onEach { packet ->
// Forward hex to JS receiver
val hex = DataByteArray(packet.value).toHexString()
webview.callHandler("monitorCharacteristic", hex)
}?.launchIn(lifecycleScope)
withContext(Dispatchers.Main) { cb.onCallBack("{\"success\":true}") }
}
})Step 6. Send / disconnect
webview.registerHandler("send", BridgeHandler { data, cb ->
val json = JsonParser.parseString(data).asJsonObject
val hex = json.get("data").asString
val bytes = DataByteArray.from(hex).value
lifecycleScope.launch(Dispatchers.IO) {
writeCh?.write(bytes)
withContext(Dispatchers.Main) { cb.onCallBack("{\"success\":true}") }
}
})
webview.registerHandler("disconnect", BridgeHandler { _, cb ->
connection?.disconnect()
cb.onCallBack("{\"success\":true}")
})Step 7. JavaScript bundle (low-level adapter)
The demo’s web project already builds a bundle that initializes the SDK with env: 'lowlevel' and wires the low-level adapter. You typically do NOT need to write extra JS — just build and copy web/web_dist/ into app/src/main/assets/web_dist/ and load file:///android_asset/web_dist/index.html.
If you customize the adapter, the core idea remains: initialize with env: 'lowlevel' and forward enumerate/connect/disconnect/send/receive via the bridge.
Step 8. UI events (PIN / Passphrase)
Handle UI_EVENT in your JS bundle and respond with HardwareSDK.uiResponse. See Config Event for event wiring and the WebUSB guide for minimal, production-ready dialogs.
PIN on device:
payload: '@@ONEKEY_INPUT_PIN_IN_DEVICE'Passphrase on device:
{ passphraseOnDevice: true, value: '' }
Step 9. Checklist
Nordic BLE library version ≥ 1.1.0 to use
includeStoredBondedDevices.Register handlers before loading the HTML to avoid race conditions.
Request runtime permissions on Android 12+ before scanning/connecting.
Persist
connectId(MAC) and cachedevice_idviagetFeatures(connectId)after the first connection.
References
Message Protocol (64‑byte framing): OneKey Message Protocol
Low‑level transport plugin contract: Low-level Transport Plugin
Last updated
Was this helpful?