Skip to Content
硬件接入传输协议Android (原生)

Android:通过蓝牙连接(原生)

注意: 演示:原生 Android 示例(WebView + JSBridge + Nordic BLE)→ native-android-example  本页基于《底层传输插件》— 建议先阅读,了解 BLE 消息格式和协议详情。

本指南展示如何通过底层适配器在原生 Android 宿主中集成 @onekeyfe/hd-common-connect-sdk。JavaScript 包在 WebView 中运行;传输调用被转发到原生(Nordic BLE)并桥接回 JS。

关键:BLE 连接流程

Android BLE 需要特定的连接顺序。跳过步骤会导致静默失败。

1. ensureBonded() // 连接前先配对 (60s 超时) 2. connect() // GATT 连接 (15s 超时) 3. delay(500ms) // 等待稳定 4. discoverServices() // 服务发现 5. delay(500ms) // 发现后等待 6. startNotifications() // 带重试机制 (3 次尝试)

为什么要先配对?

OneKey 设备需要安全的 BLE 配对。不配对会导致:

  • startNotifications() 可能静默失败
  • 设备有响应但 SDK 报告”设备未找到”
  • 间歇性连接断开

连接代码示例

suspend fun connectWithBonding(deviceAddress: String) { // 步骤 1: 确保已配对 val device = bluetoothAdapter.getRemoteDevice(deviceAddress) if (device.bondState != BluetoothDevice.BOND_BONDED) { device.createBond() // 等待配对 (用户必须确认配对对话框) delay(60000) // 或使用 BroadcastReceiver 监听 BOND_BONDED } // 步骤 2: GATT 连接 val gatt = device.connectGatt(context, false, gattCallback) // 步骤 3: 等待稳定 delay(500) // 步骤 4: 发现服务 (在 onConnectionStateChange 中触发) gatt.discoverServices() // 步骤 5: 发现后等待 delay(500) // 步骤 6: 带重试启动通知 startNotificationsWithRetry(gatt, maxRetries = 3) } suspend fun startNotificationsWithRetry(gatt: BluetoothGatt, maxRetries: Int) { repeat(maxRetries) { attempt -> try { gatt.setCharacteristicNotification(notifyCharacteristic, true) // 写入描述符以启用通知 val descriptor = notifyCharacteristic.getDescriptor(CCCD_UUID) descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt.writeDescriptor(descriptor) return // 成功 } catch (e: Exception) { delay((attempt + 1) * 1000L) // 递增退避 } } throw Exception("$maxRetries 次尝试后启动通知失败") }

推荐超时时间

操作超时
配对 (用户确认)60 秒
GATT 连接15 秒
连接后延迟500ms
服务发现15 秒
发现后延迟500ms
通知设置15 秒
重试退避1s, 2s, 3s

关键库:

  • WebView JS 桥接: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
    • 原因:BleScannerSettings 中的 includeStoredBondedDevices 需要 > 1.0.9

OneKey BLE UUID:参见 底层传输插件 - 快速参考

步骤 1. Gradle 和 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+ 权限(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"/>

运行时权限(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 }

步骤 2. 构建 Web 包(演示中已实现)

在 hardware-js-sdk 仓库中:

cd packages/connect-examples/native-android-example/web yarn && yarn build # 输出 web/web_dist/

将整个输出文件夹复制到您的 Android 项目中:

  • 复制 web/web_dist/app/src/main/assets/web_dist/
  • 入口文件将可用于:app/src/main/assets/web_dist/index.html

原生应用中不需要进一步的 JS 工作;演示的 Web 包已经使用 env: 'lowlevel' 初始化并连接了底层适配器。

步骤 3. WebView + 桥接(加载前注册)

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() // 在加载 HTML 之前注册原生处理程序 registerHandlers() // 加载复制到 assets/web_dist/ 的构建 HTML 包 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 } }

步骤 4. BLE 扫描(包括存储的已绑定设备)

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, // 需要 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) }

enumerate 的桥接处理程序:

private fun registerHandlers() { webview.registerHandler("enumerate", BridgeHandler { _, cb -> startScan { list -> cb.onCallBack(Gson().toJson(list)) } }) // 其余处理程序如下所示 }

步骤 5. 连接、特征、通知

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 -> // 将十六进制转发给 JS 接收器 val hex = DataByteArray(packet.value).toHexString() webview.callHandler("monitorCharacteristic", hex) }?.launchIn(lifecycleScope) withContext(Dispatchers.Main) { cb.onCallBack("{\"success\":true}") } } })

步骤 6. 发送 / 断开

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}") })

步骤 7. JavaScript 包(底层适配器)

演示的 Web 项目已经构建了一个使用 env: 'lowlevel' 初始化 SDK 并连接底层适配器的包。您通常不需要编写额外的 JS — 只需构建并将 web/web_dist/ 复制到 app/src/main/assets/web_dist/ 并加载 file:///android_asset/web_dist/index.html

如果您自定义适配器,核心思想保持不变:使用 env: 'lowlevel' 初始化并通过桥接转发 enumerate/connect/disconnect/send/receive

步骤 8. UI 事件(PIN / Passphrase)

在您的 JS 包中处理 UI_EVENT 并使用 HardwareSDK.uiResponse 响应。参见 事件配置 了解事件连接,以及 WebUSB 指南了解最小的、生产就绪的对话框。

  • 设备上的 PIN:payload: '@@ONEKEY_INPUT_PIN_IN_DEVICE'
  • 设备上的 Passphrase:{ passphraseOnDevice: true, value: '' }

步骤 9. 检查清单

  • Nordic BLE 库版本 >= 1.1.0 以使用 includeStoredBondedDevices
  • 在加载 HTML 之前注册处理程序以避免竞态条件。
  • 在 Android 12+ 上扫描/连接之前请求运行时权限。
  • 持久化 connectId(MAC)并在首次连接后通过 getFeatures(connectId) 缓存 device_id

参考

Last updated on