- 增加了wrapper处理原生app的交互
This commit is contained in:
28
wrappers/android/wrapper/build.gradle.kts
Normal file
28
wrappers/android/wrapper/build.gradle.kts
Normal 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")
|
||||
}
|
||||
0
wrappers/android/wrapper/consumer-rules.pro
Normal file
0
wrappers/android/wrapper/consumer-rules.pro
Normal file
2
wrappers/android/wrapper/src/main/AndroidManifest.xml
Normal file
2
wrappers/android/wrapper/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.duooomi.ble.wrapper" />
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user