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.0no.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。
参考
- 底层传输插件 — BLE 协议、消息格式、UUID 参考