Skip to Content

Android: Connect via Bluetooth (Native)

Note: Demo: Native Android example (WebView + JSBridge + Nordic BLE) → native-android-example  This guide builds on the Low-Level Transport Plugin and its 64-byte message protocol—read that first.

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.7
  • Nordic BLE (Kotlin, ≥ 1.1.0):
    • no.nordicsemi.android.kotlin.ble:scanner:1.1.0
    • no.nordicsemi.android.kotlin.ble:client:1.1.0
    • Reason: includeStoredBondedDevices in BleScannerSettings requires > 1.0.9

OneKey BLE UUIDs:

  • Service: 00000001-0000-1000-8000-00805f9b34fb
  • Write: 00000002-0000-1000-8000-00805f9b34fb
  • Notify: 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 cache device_id via getFeatures(connectId) after the first connection.

References

Last updated on