- 增加了wrapper处理原生app的交互

This commit is contained in:
Yudi Xiao
2026-04-08 17:04:29 +08:00
parent 90dcd9605c
commit f9dfde0ace
19 changed files with 1501 additions and 32 deletions

View File

@@ -0,0 +1,28 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.duooomi.ble.wrapper"
compileSdk = 34
defaultConfig {
minSdk = 24
consumerProguardFiles("consumer-rules.pro")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation("com.duooomi:duooomi-ble-sdk:1.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.duooomi.ble.wrapper" />

View File

@@ -0,0 +1,341 @@
package com.duooomi.ble.wrapper
import expo.modules.brownfield.BrownfieldMessage
import expo.modules.brownfield.BrownfieldMessaging
import expo.modules.brownfield.BrownfieldState
import expo.modules.brownfield.Removable
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
/** 表示 `DuooomiBleManager` 异步命令调用失败。 */
class DuooomiBleManagerException(message: String) : Exception(message)
/**
* 原生宿主调用的 BLE 门面。
*
* 该类型负责把宿主方法调用转换为 `expo-brownfield` 消息,
* 并将 JS 侧共享状态变化转发给宿主回调。
*/
class DuooomiBleManager {
companion object {
private const val BT_STATE = "btState"
private const val CONNECTED_DEVICE = "connectedDevice"
private const val DEVICE_INFO = "deviceInfo"
private const val VERSION = "version"
private const val IS_ACTIVATED = "isActivated"
private const val TRANSFER_PROGRESS = "transferProgress"
private const val ERROR = "error"
private const val DISCOVERED_DEVICES = "discoveredDevices"
}
private val sharedStateSubscriptions = mutableListOf<Removable>()
private var messageListenerId: String? = null
private var started = false
private val pendingRequests = ConcurrentHashMap<String, CancellableContinuation<Map<String, Any?>>>()
/** 接收 JS 侧原始消息事件。 */
var onMessage: ((BrownfieldMessage) -> Unit)? = null
/** 接收蓝牙状态变化,例如 `idle`、`scanning`、`connected`。 */
var onBluetoothStateChange: ((String) -> Unit)? = null
/** 接收当前已连接设备信息。 */
var onConnectedDeviceChange: ((DuooomiConnectedDevice) -> Unit)? = null
/** 接收设备详情信息。 */
var onDeviceInfoChange: ((DuooomiDeviceInfo) -> Unit)? = null
/** 接收固件版本信息。 */
var onVersionChange: ((String) -> Unit)? = null
/** 接收设备激活状态变化。 */
var onActivationChange: ((Boolean) -> Unit)? = null
/** 接收文件传输进度,范围为 `0..100`。 */
var onTransferProgressChange: ((Int) -> Unit)? = null
/** 接收 SDK 上报的错误字符串。 */
var onError: ((String) -> Unit)? = null
/** 接收扫描发现的设备列表。 */
var onDiscoveredDevicesChange: ((List<DuooomiDiscoveredDevice>) -> Unit)? = null
/**
* 开始订阅 Brownfield 消息与共享状态。
*
* 调用前应先通过 [DuooomiBleRuntime] 启动 RN/Brownfield 运行时。
*/
fun start() {
if (started) return
started = true
sharedStateSubscriptions += BrownfieldState.subscribe(BT_STATE) { value ->
(value as? String)?.let { onBluetoothStateChange?.invoke(it) }
}
sharedStateSubscriptions += BrownfieldState.subscribe(CONNECTED_DEVICE) { value ->
@Suppress("UNCHECKED_CAST")
(value as? Map<String, Any?>)?.let {
onConnectedDeviceChange?.invoke(
DuooomiConnectedDevice(
id = DuooomiBleValueDecoder.requiredString(it, "id"),
name = DuooomiBleValueDecoder.optionalString(it, "name"),
)
)
}
}
sharedStateSubscriptions += BrownfieldState.subscribe(DEVICE_INFO) { value ->
@Suppress("UNCHECKED_CAST")
(value as? Map<String, Any?>)?.let {
onDeviceInfoChange?.invoke(
DuooomiDeviceInfo(
allspace = DuooomiBleValueDecoder.requiredString(it, "allspace"),
freespace = DuooomiBleValueDecoder.requiredString(it, "freespace"),
name = DuooomiBleValueDecoder.requiredString(it, "name"),
size = DuooomiBleValueDecoder.requiredString(it, "size"),
brand = DuooomiBleValueDecoder.requiredString(it, "brand"),
powerlevel = DuooomiBleValueDecoder.requiredInt(it, "powerlevel"),
)
)
}
}
sharedStateSubscriptions += BrownfieldState.subscribe(VERSION) { value ->
(value as? String)?.let { onVersionChange?.invoke(it) }
}
sharedStateSubscriptions += BrownfieldState.subscribe(IS_ACTIVATED) { value ->
(value as? Boolean)?.let { onActivationChange?.invoke(it) }
}
sharedStateSubscriptions += BrownfieldState.subscribe(TRANSFER_PROGRESS) { value ->
(value as? Int)?.let { onTransferProgressChange?.invoke(it) }
}
sharedStateSubscriptions += BrownfieldState.subscribe(ERROR) { value ->
(value as? String)?.let { onError?.invoke(it) }
}
sharedStateSubscriptions += BrownfieldState.subscribe(DISCOVERED_DEVICES) { value ->
@Suppress("UNCHECKED_CAST")
(value as? List<Map<String, Any?>>)?.let { devices ->
onDiscoveredDevicesChange?.invoke(
devices.mapNotNull { device ->
runCatching {
DuooomiDiscoveredDevice(
id = DuooomiBleValueDecoder.requiredString(device, "id"),
name = DuooomiBleValueDecoder.optionalString(device, "name"),
rssi = DuooomiBleValueDecoder.optionalInt(device, "rssi"),
)
}.getOrNull()
}
)
}
}
messageListenerId = BrownfieldMessaging.addListener { event ->
if (handleResponse(event)) {
return@addListener
}
onMessage?.invoke(event)
}
}
/** 停止订阅 Brownfield 消息与共享状态。 */
fun stop() {
started = false
sharedStateSubscriptions.forEach { it.remove() }
sharedStateSubscriptions.clear()
pendingRequests.forEach { (_, continuation) ->
continuation.resumeWithException(DuooomiBleManagerException("Manager stopped before response was received"))
}
pendingRequests.clear()
messageListenerId?.let(BrownfieldMessaging::removeListener)
messageListenerId = null
}
/** 开始扫描蓝牙设备。 */
fun scan() {
send(mapOf("action" to "scan"))
}
/** 停止扫描蓝牙设备。 */
fun stopScan() {
send(mapOf("action" to "stopScan"))
}
/**
* 连接指定设备。
*
* @param deviceId 设备唯一标识。
* @return 连接成功后的设备摘要信息。
*/
suspend fun connect(deviceId: String): DuooomiConnectedDevice {
val result = sendAsync(
action = "connect",
params = mapOf("deviceId" to deviceId),
)
return DuooomiConnectedDevice(
id = DuooomiBleValueDecoder.requiredString(result, "id"),
name = DuooomiBleValueDecoder.optionalString(result, "name"),
)
}
/**
* 断开当前设备连接。
*
* @return 断开连接结果。
*/
suspend fun disconnect(): DuooomiCommandResult {
val result = sendAsync(action = "disconnect")
return DuooomiCommandResult(
success = DuooomiBleValueDecoder.requiredBoolean(result, "success"),
)
}
/**
* 获取当前已连接设备的详细信息。
*
* @return 设备详情数据。
*/
suspend fun getDeviceInfo(): DuooomiDeviceInfo {
val result = sendAsync(action = "getDeviceInfo")
return DuooomiDeviceInfo(
allspace = DuooomiBleValueDecoder.requiredString(result, "allspace"),
freespace = DuooomiBleValueDecoder.requiredString(result, "freespace"),
name = DuooomiBleValueDecoder.requiredString(result, "name"),
size = DuooomiBleValueDecoder.requiredString(result, "size"),
brand = DuooomiBleValueDecoder.requiredString(result, "brand"),
powerlevel = DuooomiBleValueDecoder.requiredInt(result, "powerlevel"),
)
}
/**
* 获取当前已连接设备的版本信息。
*
* @return 版本信息数据。
*/
suspend fun getVersion(): DuooomiVersionInfo {
val result = sendAsync(action = "getVersion")
return DuooomiVersionInfo(
version = DuooomiBleValueDecoder.requiredString(result, "version"),
type = DuooomiBleValueDecoder.requiredString(result, "type"),
)
}
/**
* 绑定当前设备到指定用户。
*
* @param userId 业务侧用户标识。
* @return 绑定结果数据。
*/
suspend fun bind(userId: String): DuooomiBindingResult {
val result = sendAsync(
action = "bind",
params = mapOf("userId" to userId),
)
return DuooomiBindingResult(
type = DuooomiBleValueDecoder.requiredInt(result, "type"),
sn = DuooomiBleValueDecoder.requiredString(result, "sn"),
success = DuooomiBleValueDecoder.requiredInt(result, "success"),
contents = DuooomiBleValueDecoder.stringList(result, "contents"),
)
}
/**
* 解绑当前设备与指定用户的关系。
*
* @param userId 业务侧用户标识。
* @return 解绑结果数据。
*/
suspend fun unbind(userId: String): DuooomiUnbindResult {
val result = sendAsync(
action = "unbind",
params = mapOf("userId" to userId),
)
return DuooomiUnbindResult(
success = DuooomiBleValueDecoder.requiredInt(result, "success"),
)
}
/**
* 删除设备中的指定文件。
*
* @param key 设备侧文件 key。
* @return 删除结果数据。
*/
suspend fun deleteFile(key: String): DuooomiDeleteFileResult {
val result = sendAsync(
action = "deleteFile",
params = mapOf("key" to key),
)
return DuooomiDeleteFileResult(
type = DuooomiBleValueDecoder.requiredInt(result, "type"),
success = DuooomiBleValueDecoder.requiredInt(result, "success"),
)
}
/**
* 向设备传输文件。
*
* @param fileUri 待传输文件的本地 URI。
* @param commandType 传输命令类型;不传时由 JS 侧使用默认值。
* @return 传输完成结果。
*/
suspend fun transferFile(fileUri: String, commandType: Int? = null): DuooomiCommandResult {
val result = sendAsync(
action = "transferFile",
params = buildMap {
put("fileUri", fileUri)
commandType?.let { put("commandType", it) }
},
)
return DuooomiCommandResult(
success = DuooomiBleValueDecoder.requiredBoolean(result, "success"),
)
}
private fun send(message: BrownfieldMessage) {
BrownfieldMessaging.sendMessage(message)
}
private suspend fun sendAsync(
action: String,
params: Map<String, Any?> = emptyMap(),
): Map<String, Any?> = suspendCancellableCoroutine { continuation ->
val requestId = UUID.randomUUID().toString()
pendingRequests[requestId] = continuation
continuation.invokeOnCancellation {
pendingRequests.remove(requestId)
}
send(
buildMap {
putAll(params)
put("action", action)
put("requestId", requestId)
}
)
}
private fun handleResponse(message: BrownfieldMessage): Boolean {
if (message["event"] != "response") {
return false
}
val requestId = message["requestId"] as? String ?: return false
val continuation = pendingRequests.remove(requestId) ?: return true
val success = message["success"] as? Boolean ?: false
if (success) {
@Suppress("UNCHECKED_CAST")
val data = message["data"] as? Map<String, Any?> ?: emptyMap()
continuation.resume(data)
} else {
val error = message["error"] as? String ?: "Unknown error"
continuation.resumeWithException(DuooomiBleManagerException(error))
}
return true
}
}

View File

@@ -0,0 +1,99 @@
package com.duooomi.ble.wrapper
/** 扫描阶段发现的设备摘要信息。 */
data class DuooomiDiscoveredDevice(
val id: String,
val name: String?,
val rssi: Int?,
)
/** 设备连接结果摘要。 */
data class DuooomiConnectedDevice(
val id: String,
val name: String?,
)
/** 设备详细信息。 */
data class DuooomiDeviceInfo(
val allspace: String,
val freespace: String,
val name: String,
val size: String,
val brand: String,
val powerlevel: Int,
)
/** 设备版本信息。 */
data class DuooomiVersionInfo(
val version: String,
val type: String,
)
/** 绑定结果信息。 */
data class DuooomiBindingResult(
val type: Int,
val sn: String,
val success: Int,
val contents: List<String>,
)
/** 解绑结果信息。 */
data class DuooomiUnbindResult(
val success: Int,
)
/** 删除文件结果信息。 */
data class DuooomiDeleteFileResult(
val type: Int,
val success: Int,
)
/** 通用布尔完成结果。 */
data class DuooomiCommandResult(
val success: Boolean,
)
internal object DuooomiBleValueDecoder {
fun requiredString(map: Map<String, Any?>, key: String): String {
return optionalString(map, key)
?: throw DuooomiBleManagerException("Missing string value for key: $key")
}
fun optionalString(map: Map<String, Any?>, key: String): String? {
val value = map[key] ?: return null
return when (value) {
is String -> value
is Number -> value.toString()
else -> null
}
}
fun requiredInt(map: Map<String, Any?>, key: String): Int {
return optionalInt(map, key)
?: throw DuooomiBleManagerException("Missing integer value for key: $key")
}
fun optionalInt(map: Map<String, Any?>, key: String): Int? {
val value = map[key] ?: return null
return when (value) {
is Int -> value
is Number -> value.toInt()
is String -> value.toIntOrNull()
else -> null
}
}
fun requiredBoolean(map: Map<String, Any?>, key: String): Boolean {
val value = map[key]
return when (value) {
is Boolean -> value
is Number -> value.toInt() != 0
else -> throw DuooomiBleManagerException("Missing boolean value for key: $key")
}
}
fun stringList(map: Map<String, Any?>, key: String): List<String> {
@Suppress("UNCHECKED_CAST")
return map[key] as? List<String> ?: emptyList()
}
}

View File

@@ -0,0 +1,56 @@
package com.duooomi.ble.wrapper
import android.content.Context
import android.view.View
import android.view.ViewGroup
/**
* 负责在宿主视图层中挂载隐藏的 RN 容器并启动 JS 运行时。
*
* @property manager 运行时启动后对外复用的 BLE 门面实例。
*/
class DuooomiBleRuntime(
val manager: DuooomiBleManager = DuooomiBleManager(),
) {
private var mountedView: View? = null
/**
* 在指定容器中启动 Brownfield 运行时。
*
* @param container 用于挂载隐藏 React Native 视图的宿主容器。
* @param initializeHost 可选的宿主初始化逻辑。
* @param createReactNativeView 由宿主提供的 RN View 构造逻辑。
*/
fun start(
container: ViewGroup,
initializeHost: (() -> Unit)? = null,
createReactNativeView: ((Context) -> View)?
) {
if (mountedView != null) {
return
}
initializeHost?.invoke()
val reactNativeView = createReactNativeView(container.context)
reactNativeView.alpha = 0f
reactNativeView.isClickable = false
reactNativeView.isFocusable = false
reactNativeView.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
val layoutParams = ViewGroup.LayoutParams(1, 1)
container.addView(reactNativeView, layoutParams)
mountedView = reactNativeView
manager.start()
}
/** 停止运行时并移除已挂载的隐藏 RN 视图。 */
fun stop() {
manager.stop()
mountedView?.let { view ->
(view.parent as? ViewGroup)?.removeView(view)
}
mountedView = null
}
}