425 lines
15 KiB
Markdown
425 lines
15 KiB
Markdown
# 步骤 02:Bridge 层实现(✅ 已完成)
|
||
|
||
> 实现 Go ↔ Kotlin 桥接层,包括 Go 侧 TSClient 导出、Kotlin 友好封装(TsClient.kt + TsModels.kt)、应用层桥接(TSBridge.kt)。
|
||
|
||
---
|
||
|
||
## 一、目标
|
||
|
||
- [x] Go 侧 TSClient 结构体及方法导出(`go/teamspeak/bridge.go`)
|
||
- [x] gomobile 编译为 AAR(`android/app/libs/teamspeak.aar`)
|
||
- [x] Kotlin 友好封装层(`TsClient.kt` + `TsModels.kt`)
|
||
- [x] 应用层桥接(`TSBridge.kt`)
|
||
|
||
---
|
||
|
||
## 二、架构总览
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Kotlin 应用层 │
|
||
│ │
|
||
│ ViewModel ──→ TSBridge (object) ──→ TsClient (object) │
|
||
│ │ │ │
|
||
│ │ TsListener 回调 │ Kotlin 友好 API │
|
||
│ ↑ │ │
|
||
│ └── TsListener ─────────┘ │
|
||
│ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ gomobile 自动生成层 │
|
||
│ │
|
||
│ teamspeak.TSClient (Java) ← Go TSClient struct │
|
||
│ teamspeak.EventCallback (Java) ← Go EventCallback interface│
|
||
│ teamspeak.* 数据类 (Java) ← Go 导出结构体 │
|
||
│ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ Go 层 │
|
||
│ │
|
||
│ bridge.go (TSClient + EventCallback) ──→ teamspeak-go SDK │
|
||
│ │ │ │
|
||
│ │ 事件队列 + 单消费者 goroutine │ On* 回调 │
|
||
│ └──→ JNI 回调 ──→ EventCallback │ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**数据流**:
|
||
|
||
| 方向 | 路径 | 说明 |
|
||
| --- | --- | --- |
|
||
| Kotlin → Go | `TSBridge` → `TsClient` → gomobile `TSClient` → Go `Client` | 用户操作 |
|
||
| Go → Kotlin | Go `On*` → 事件队列 → JNI → `EventCallback` → `TsClient` 转换 → `TsListener` → `TSBridge` | 事件推送 |
|
||
|
||
---
|
||
|
||
## 三、三层架构详解
|
||
|
||
### 3.1 Go 层(bridge.go)
|
||
|
||
**文件**:`go/teamspeak/bridge.go`
|
||
|
||
Go 侧 Bridge 是 gomobile 导出的核心,将 teamspeak-go SDK 封装为可供 Kotlin 调用的 `TSClient` 类。
|
||
|
||
#### 导出的接口与结构
|
||
|
||
```go
|
||
// EventCallback — gomobile 导出的回调接口
|
||
type EventCallback interface {
|
||
OnConnected()
|
||
OnDisconnected(message string)
|
||
OnTextMessage(msg *TextMsg)
|
||
OnClientEnter(client *ServerClientView)
|
||
OnClientLeave(client *ServerClientView)
|
||
OnClientMoved(client *ClientMoved)
|
||
OnKicked(reason *ServerError)
|
||
OnTalkStatusChanged(talker *TalkStatusChange)
|
||
OnClientIDsDone()
|
||
OnServerError(error *ServerError)
|
||
}
|
||
|
||
// TSClient — gomobile 导出的桥接客户端
|
||
type TSClient struct {
|
||
client *teamspeak.Client
|
||
callback EventCallback
|
||
}
|
||
|
||
// 导出的数据结构(适配 gomobile 限制)
|
||
type ServerClientView struct { ... } // 频道成员视图
|
||
type ClientInfo struct { ... } // 完整客户端信息
|
||
type ChannelListItem struct { ... } // 频道列表项
|
||
type TextMsg struct { ... } // 文本消息
|
||
type ClientMoved struct { ... } // 客户端移动事件
|
||
type ServerError struct { ... } // 服务器错误
|
||
type TalkStatusChange struct { ... } // 说话状态变化
|
||
type ChannelListResult struct { ... } // 频道列表结果(含数组)
|
||
```
|
||
|
||
#### 关键设计决策
|
||
|
||
1. **字符串传递 ID**:gomobile 不支持 `uint64` → Kotlin Long 的安全映射,统一用 `string`
|
||
2. **结构体包装**:gomobile 不支持 `[]*T`,用 `ChannelListResult` 包装 `ChannelListItem[]`
|
||
3. **错误用字符串**:gomobile 不支持 `error` 返回值,用空串=成功
|
||
4. **事件队列**:SDK 内部 `evtQueue` + 单消费者 goroutine 串行分发,JNI 回调线程安全
|
||
|
||
### 3.2 Kotlin 友好封装层(TsClient.kt + TsModels.kt)
|
||
|
||
**文件**:`android/app/src/main/java/com/tsmobile/app/TsClient.kt`、`TsModels.kt`
|
||
|
||
这一层将 gomobile 生成的 Java 对象转为 Kotlin 友好接口,解决:
|
||
- 所有 API 为 getter/setter 而非属性
|
||
- `ChannelListItem[]` 需要手动转 `List<TsChannel>`
|
||
- 事件回调参数是 `Object` 类型需要强转
|
||
- group 类型是 `Int` 需要映射为 `ChanGroupType` 枚举
|
||
|
||
#### TsClient 对象
|
||
|
||
```kotlin
|
||
object TsClient {
|
||
// 连接
|
||
fun connect(identity, host, nickname, password, defaultChannel): Boolean
|
||
fun disconnect()
|
||
|
||
// 事件监听
|
||
fun setListener(listener: TsListener?)
|
||
|
||
// 查询(返回 Kotlin 友好类型)
|
||
fun getChannelList(): List<TsChannel>
|
||
fun getClientList(): List<TsClientInfo>
|
||
fun getClientId(): Long
|
||
fun getClientInfo(clientId: Long): TsClientInfo?
|
||
fun getChannelInfo(channelId: Long): TsChannel?
|
||
fun getSelf(): TsSelf
|
||
fun getChannelIdsByUid(uid: String, maxDepth: Int): List<Long>
|
||
|
||
// 属性
|
||
val serverVersion: String
|
||
val serverIp: String
|
||
val serverPlatform: String
|
||
val serverName: String
|
||
val serverCreated: Long
|
||
val serverUptime: Long
|
||
val maxClients: Int
|
||
val clientsOnline: Int
|
||
val channelsOnline: Int
|
||
|
||
// 操作
|
||
fun sendTextMessage(targetMode: Int, targetId: Long, msg: String): Boolean
|
||
fun clientMove(clientId: Long, channelId: Long, password: String): Boolean
|
||
fun clientPoke(clientId: Long, msg: String): Boolean
|
||
fun kickClient(clientId: Long, reasonId: Int, reasonMsg: String): Boolean
|
||
fun banClient(uid: String, timeInSeconds: Long, reasonMsg: String): Boolean
|
||
fun channelCreate(name: String, properties: Map<String, String>, permissions: List<TSPermission>): Long
|
||
fun channelUpdate(channelId: Long, properties: Map<String, String>): Boolean
|
||
fun channelDelete(channelId: Long, force: Boolean): Boolean
|
||
fun sendVoice(clientId: Long, codec: Int, data: ByteArray): Boolean
|
||
}
|
||
```
|
||
|
||
#### TsListener 接口
|
||
|
||
```kotlin
|
||
interface TsListener {
|
||
fun onConnected()
|
||
fun onDisconnected(error: String?)
|
||
fun onClientEnter(client: TsClientInfo)
|
||
fun onClientMoved(moved: TsClientMoved)
|
||
fun onClientLeave(client: ServerClientView)
|
||
fun onTalkStatusChanged(talker: TsTalker)
|
||
fun onClientIDsDone()
|
||
fun onTextMessage(msg: TsTextMessage)
|
||
fun onPoked(msg: TsTextMessage)
|
||
fun onKicked(reason: ServerError?)
|
||
fun onServerError(error: ServerError)
|
||
fun onChannelListChanged()
|
||
}
|
||
```
|
||
|
||
#### TsModels 数据类
|
||
|
||
```kotlin
|
||
// 频道
|
||
data class TsChannel(val channelListItem: ChannelListItem) {
|
||
val channelId get() = channelListItem.getChannelID()
|
||
val parentId get() = channelListItem.getParentChannelID()
|
||
val name get() = channelListItem.getName()
|
||
val order get() = channelListItem.getOrder()
|
||
val isPermanent get() = channelListItem.getIsPermanent()
|
||
val totalClients get() = channelListItem.getTotalClients()
|
||
// ... 更多属性
|
||
}
|
||
|
||
// 客户端
|
||
data class TsClientInfo(val serverClientView: ServerClientView) {
|
||
val clientId get() = serverClientView.getID()
|
||
val channelId get() = serverClientView.getChannelID()
|
||
val nickname get() = serverClientView.getNickname()
|
||
val uid get() = serverClientView.getUid()
|
||
val isTalker get() = serverClientView.getIsTalker()
|
||
// ... 更多属性
|
||
}
|
||
|
||
// 自身信息(可写)
|
||
class TsSelf internal constructor(
|
||
private val view: ServerClientView,
|
||
private val info: ClientInfo?
|
||
) {
|
||
var nickname
|
||
get() = view.getNickname()
|
||
set(value) { info?.setNickname(value) }
|
||
var isTalker
|
||
get() = view.getIsTalker()
|
||
set(value) { info?.setIsTalker(value) }
|
||
var inputMuted
|
||
get() = info?.getInputMuted() ?: false
|
||
set(value) { info?.setInputMuted(value) }
|
||
var outputMuted
|
||
get() = info?.getOutputMuted() ?: false
|
||
set(value) { info?.setOutputMuted(value) }
|
||
// ... 更多可写属性
|
||
}
|
||
|
||
// 移动事件
|
||
data class TsClientMoved(
|
||
val theClient: ServerClientView?,
|
||
val oldChannel: ChannelListItem?,
|
||
val newChannel: ChannelListItem?,
|
||
val visibility: Int
|
||
)
|
||
|
||
// 说话状态
|
||
data class TsTalker(
|
||
val client: ServerClientView?,
|
||
val isTalking: Boolean
|
||
)
|
||
|
||
// 文本消息
|
||
data class TsTextMessage(
|
||
val message: String,
|
||
val invokerUid: String,
|
||
val invokerName: String,
|
||
val invokerId: Long,
|
||
val targetMode: Int,
|
||
val targetClientId: Long,
|
||
val targetChannelId: Long
|
||
)
|
||
|
||
// 频道组/服务器组类型
|
||
enum class ChanGroupType(val value: Int) { ... }
|
||
enum class ChannelGroupType(val value: Int) { ... }
|
||
```
|
||
|
||
### 3.3 应用层桥接(TSBridge.kt)
|
||
|
||
**文件**:`android/app/src/main/java/com/tsmobile/app/TSBridge.kt`
|
||
|
||
TSBridge 是 ViewModel 层访问 Go 桥接的唯一入口,采用 `object` 单例模式。
|
||
|
||
#### 核心职责
|
||
|
||
1. **连接管理**:封装 `TsClient.connect()` / `disconnect()`
|
||
2. **监听注册**:在 `connect()` 时自动设置 `TsListener`
|
||
3. **查询转发**:所有查询方法委托给 `TsClient`
|
||
4. **操作转发**:所有操作方法委托给 `TsClient`
|
||
|
||
#### 完整 API
|
||
|
||
```kotlin
|
||
object TSBridge {
|
||
// === 连接管理 ===
|
||
fun connect(identity: Any, host: String, nickname: String,
|
||
password: String = "", defaultChannel: String = "",
|
||
listener: TSBridgeListener? = null): Boolean
|
||
fun disconnect()
|
||
|
||
// === 查询 ===
|
||
fun getChannelList(): List<TsChannel>
|
||
fun getClientList(): List<TsClientInfo>
|
||
fun getClientId(): Long
|
||
fun getSelf(): TsSelf
|
||
fun getChannelInfo(channelId: Long): TsChannel?
|
||
fun getClientInfo(clientId: Long): TsClientInfo?
|
||
fun getChannelIdsByUid(uid: String): List<Long>
|
||
|
||
// === 操作 ===
|
||
fun sendTextMessage(targetMode: Int, targetId: Long, msg: String): Boolean
|
||
fun clientMove(clientId: Long, channelId: Long, password: String = ""): Boolean
|
||
fun clientPoke(clientId: Long, msg: String): Boolean
|
||
fun kickClient(clientId: Long, reasonId: Int, reasonMsg: String): Boolean
|
||
fun banClient(uid: String, timeInSeconds: Long, reasonMsg: String): Boolean
|
||
fun channelCreate(name: String, properties: Map<String, String> = emptyMap(),
|
||
permissions: List<TSPermission> = emptyList()): Long
|
||
fun channelUpdate(channelId: Long, properties: Map<String, String>): Boolean
|
||
fun channelDelete(channelId: Long, force: Boolean = false): Boolean
|
||
fun sendVoice(clientId: Long, codec: Int, data: ByteArray): Boolean
|
||
|
||
// === 服务器属性 ===
|
||
val serverVersion: String
|
||
val serverIp: String
|
||
val serverPlatform: String
|
||
val serverName: String
|
||
// ...
|
||
}
|
||
|
||
// 回调接口(简化版,供 ViewModel 使用)
|
||
interface TSBridgeListener {
|
||
fun onConnected()
|
||
fun onDisconnected(error: String?)
|
||
fun onClientEnter(client: TsClientInfo)
|
||
fun onClientMoved(moved: TsClientMoved)
|
||
fun onClientLeave(client: ServerClientView)
|
||
fun onTalkStatusChanged(talker: TsTalker)
|
||
fun onClientIDsDone()
|
||
fun onTextMessage(msg: TsTextMessage)
|
||
fun onPoked(msg: TsTextMessage)
|
||
fun onKicked(reason: ServerError?)
|
||
fun onServerError(error: ServerError)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四、数据流转详解
|
||
|
||
### 4.1 连接流程
|
||
|
||
```
|
||
ViewModel: TSBridge.connect(identity, host, nickname, password)
|
||
↓
|
||
TSBridge: TsClient.connect(identity, host, nickname, password)
|
||
↓
|
||
TsClient: TSClient.connect(...) // gomobile Java 对象
|
||
↓
|
||
Go: TSClient.Connect() → NewClient → registerHandlers → Connect → WaitConnected
|
||
↓
|
||
Go SDK: 收到 welcome → 触发 OnConnected
|
||
↓
|
||
Go Bridge: EventCallback.OnConnected() // JNI
|
||
↓
|
||
TsClient: listener?.onConnected() // Kotlin 友好回调
|
||
↓
|
||
TSBridge: listener?.onConnected()
|
||
↓
|
||
ViewModel: _state.update { it.copy(connected = true) }
|
||
```
|
||
|
||
### 4.2 查询流程
|
||
|
||
```
|
||
ViewModel: TSBridge.getChannelList()
|
||
↓
|
||
TSBridge: TsClient.getChannelList()
|
||
↓
|
||
TsClient: TSClient.requestChannelList() // gomobile
|
||
↓
|
||
TsClient: result.getChannels().map { TsChannel(it) } // 转为 Kotlin 类型
|
||
↓
|
||
TSBridge: 返回 List<TsChannel>
|
||
↓
|
||
ViewModel: _state.update { it.copy(channels = list) }
|
||
```
|
||
|
||
### 4.3 事件推送流程
|
||
|
||
```
|
||
TeamSpeak 服务器: notifyclientmoved
|
||
↓
|
||
Go SDK: handleNotification → evtQueue → startEventLoop
|
||
↓
|
||
Go Bridge: EventCallback.OnClientMoved(clientMoved)
|
||
↓ JNI
|
||
TsClient: TsListener.onClientMoved(TsClientMoved(view, old, new, vis))
|
||
↓
|
||
TSBridge: listener?.onClientMoved(moved)
|
||
↓
|
||
ViewModel: 处理移动事件,更新状态
|
||
```
|
||
|
||
---
|
||
|
||
## 五、gomobile 约束与应对
|
||
|
||
| 约束 | 影响 | 应对方案 |
|
||
| --- | --- | --- |
|
||
| 不能导出 `[]string` | 服务器组列表无法直接传递 | `TsClientInfo` 封装为逗号分隔字符串 |
|
||
| 不能导出 `[]*T` | 频道/客户端列表无法返回切片 | `ChannelListResult` 包装 + `TsClient` 转 `List` |
|
||
| 不能导出 `map[string]string` | 属性列表无法传递 | `channelCreate`/`channelUpdate` 接受 `Map`,内部转 gomobile 类型 |
|
||
| 不能导出 `error` | 方法无法返回错误 | `Boolean` 返回值(true=成功) |
|
||
| `uint64` 映射为 `long` | 频道 ID 可能溢出 | 统一用 `string` 传递 ID |
|
||
| 回调在 JNI 线程 | 不能直接操作 UI | `TsListener` 回调 → ViewModel + StateFlow 中转 |
|
||
| 所有字段为 getter/setter | Kotlin 不友好 | `TsModels` 包装为 `val`/`var` 属性 |
|
||
|
||
---
|
||
|
||
## 六、文件清单
|
||
|
||
| 文件 | 说明 |
|
||
| --- | --- |
|
||
| `go/teamspeak/bridge.go` | Go 侧桥接,导出 TSClient + EventCallback |
|
||
| `android/app/libs/teamspeak.aar` | gomobile 编译产物 |
|
||
| `android/app/src/main/java/com/tsmobile/app/TsClient.kt` | Kotlin 友好封装(TsClient 对象) |
|
||
| `android/app/src/main/java/com/tsmobile/app/TsModels.kt` | Kotlin 数据类(TsChannel, TsClientInfo 等) |
|
||
| `android/app/src/main/java/com/tsmobile/app/TSBridge.kt` | 应用层桥接(单例) |
|
||
|
||
---
|
||
|
||
## 七、验收标准
|
||
|
||
| # | 验证项 | 验证方法 |
|
||
| --- | --- | --- |
|
||
| 1 | bridge.go 可编译 | `cd go && go build ./teamspeak` 无报错 |
|
||
| 2 | gomobile 生成 AAR | `gomobile bind ...` 成功 |
|
||
| 3 | TsClient.connect() 可调用 | 编译通过,连接测试服务器成功 |
|
||
| 4 | TsListener 回调正常 | 连接后收到 `onConnected`、`onClientEnter` 等 |
|
||
| 5 | 查询返回 Kotlin 类型 | `getChannelList()` 返回 `List<TsChannel>` |
|
||
| 6 | 操作方法正常 | `sendTextMessage`、`clientMove` 等返回 true |
|
||
| 7 | 断开连接无崩溃 | `disconnect()` 后应用正常退出 |
|
||
|
||
---
|
||
|
||
## 八、参考文档
|
||
|
||
- `docs/sdk-bridge-api.md` — TsClient API 完整参考
|
||
- `docs/流程/00_总览.md` — 运行架构、三条核心通道
|
||
- `docs/流程/01_连接服务器.md` — 连接时序、事件依赖
|
||
- `CLAUDE.md` — gomobile 限制说明、JNI 线程注意事项
|