主题
晞晗IM(XHIM)Android 接入指南
第一次在空白 Android 工程接入? 请先看 Android 从零快速接入。本页保留 JNI、C ABI、发布和高级 二次开发细节。
listDeviceSessions、revokeDeviceSession、请求取消及同步只读策略示例见 Android QuickStart 的登录设备管理。
当前状态:Gradle Library、动态注册 JNI Bridge、Kotlin coroutine/Flow Facade、Compose 会话/聊天组件、可选通话控件和 QuickStart 已提交。JNI 已通过严格 C++ 语法门禁;正式售卖前仍需在固定 Android SDK/NDK 上构建多 ABI AAR, 完成真机与 Maven 消费测试。
1. 商用交付边界
Android 客户最终只依赖 AAR/Maven 包:
text
xhim-sdk.aar
├── classes.jar Kotlin Headless Facade
├── jni/arm64-v8a/libxhim_jni.so
├── jni/armeabi-v7a/libxhim_jni.so 仅在支持矩阵声明时提供
├── jni/x86_64/libxhim_jni.so Emulator
├── prefab/ 可选,仅供 NDK 深度客户
├── R.txt / AndroidManifest.xml
├── consumer-rules.pro
└── classes.jar/META-INF/
├── XHIM-LICENSE.txt
└── XHIM-NOTICE.txt建议拆分 Maven artifact:
text
<group>:xhim-sdk:<version> Headless Kotlin Facade + native core
<group>:xhim-ui-compose:<version> Compose UI,可选
<group>:xhim-ui-view:<version> View UI,可选
<group>:xhim-call:<version> 系统通话桥/RTC,可选真实 group ID、namespace、Maven 仓库和最低 API Level 尚未冻结,发布前必须进入 品牌与兼容性清单。
Android 官方支持在 AAR 中携带 native libraries,并可通过 Prefab 向 C/C++ 消费者导出头和库。参考: AAR Native Dependencies 与 Prefab。
2. 客户项目接入(目标发行包)
正式发布后在 settings.gradle.kts 配置企业 Maven 仓库,然后:
kotlin
dependencies {
implementation("<group>:xhim-sdk:<version>")
// 可选:
implementation("<group>:xhim-ui-compose:<version>")
}App 不应再声明 externalNativeBuild 编译 XHIM,也不应复制 .so 到业务仓库。 SDK AAR 必须自己提供所有声明支持的 ABI,并避免把同名不同版本 libc++_shared.so 与其他三方 SDK 冲突地打进 APK。
使用 abiFilters 时必须覆盖 XHIM 发布矩阵:
kotlin
android {
defaultConfig {
ndk {
abiFilters += setOf("arm64-v8a", "x86_64")
}
}
}是否保留 32 位 ABI 由产品支持矩阵决定,不能只构建成功一个 ABI 就宣称 “支持 Android”。
3. 当前源码验证
当前仓库的 Android Library 已按下列结构建立:
text
platforms/android/xhim-sdk/
├── build.gradle.kts
├── src/main/kotlin/... Kotlin Facade
├── src/main/cpp/xhim_jni.cpp
└── src/main/cpp/CMakeLists.txtJNI .so 推荐静态链接 XHIM Core 和 SQLite,只向 JVM 暴露一个 native library。 Android NDK 环境不能误用主机 SQLite;产品构建应提供经过许可证审计的官方 SQLite amalgamation,并使用 XHIM_SQLITE_PROVIDER=BUNDLED,或注入经过验证的 目标 ABI SQLite::SQLite3。
构建必须锁定 NDK、CMake、Android Gradle Plugin、JDK 和 STL 策略。升级任何 一项都单独跑全 ABI 与真机回归,不与普通业务版本捆绑升级。 当前仓库还没有 androidTest 真机消费套件;它属于正式 AAR 发布前必须补齐的 验收门禁,不能由 JNI 主机语法检查替代。
4. Kotlin Facade API
建议公开 coroutine/Flow,而不是 JNI 指针或 callback:
kotlin
// 实际入口:xhim-sdk/.../XHIMClient.kt
val client = XHIMClient.connect(
context = applicationContext,
server = "https://im.customer.example",
userId = "alice",
)
val policy = XHIMRuntimePolicy(
requestTimeoutMilliseconds = 15_000,
syncPageSize = 200,
reconnectInitialDelayMilliseconds = 1_000,
reconnectMaxDelayMilliseconds = 30_000,
maxReconnectAttempts = 8,
)
class XHIMClient : AutoCloseable {
val events: SharedFlow<XHIMEvent>
companion object {
suspend fun connect(
context: Context,
server: String,
userId: String,
appId: String? = null,
authentication: XHIMAuthentication =
XHIMAuthentication.Development,
storageRoot: File? = null
): XHIMClient
}
suspend fun start()
suspend fun login(accountHint: String, accessToken: String)
suspend fun updateCredential(accessToken: String)
suspend fun logout()
suspend fun sendText(
conversationId: String,
clientMessageId: String? = null,
text: String
): XHIMSendReceipt
suspend fun sendMessage(
conversationId: String,
message: XHIMOutgoingMessage,
clientMessageId: String? = null
): XHIMSendReceipt
suspend fun retryMessage(clientMessageId: String): XHIMSendReceipt
suspend fun cancelMessage(clientMessageId: String): XHIMSendReceipt
suspend fun message(clientMessageId: String): XHIMMessage
suspend fun messages(
conversationId: String,
cursor: ByteArray? = null,
limit: Int = 50
): XHIMMessagePage
suspend fun conversations(
cursor: ByteArray? = null,
limit: Int = 50
): XHIMConversationPage
suspend fun markConversationRead(
conversationId: String,
throughServerSequence: Long
): XHIMConversationReadReceipt
fun state(): XHIMClientState
suspend fun shutdown()
}持久 UI 不轮询数据库:XHIMEvent 公开 MessageUpserted/MessageStateChanged/ConversationChanged/SocialChanged/ SyncApplied,统一 change 含 origin、scope、IDs、revision、sequence 和账号 fence。Compose 可用 XHIMProjectionRequeryController(client, viewModelScope) 订阅后先查询、再按失效通知调用公开 SDK 查询。未知 kind/schema 保留为 ProjectionInvalidated 并宽范围重查;JNI 回调内已完成深拷贝,collector 使用 自己的协程上下文,UI 不读取 SQLite。
运行策略和 XHIMDeployment 在 Client 创建时复制,账号生命周期内不可变。 客户可提供 Bootstrap URL、Endpoint Key ID 和 Ed25519 公钥;API/WSS/上传/ 下载地址只能来自验签且未过期的 Endpoint Bundle。TLS Trust 不暴露“关闭校验” 之类的 App 配置。
xhim-ui-compose 已提供 XHIMConversationList、XHIMChat 和 XHIMCallControls。它只依赖 xhim-sdk,业务可以完全不引入 UI artifact。
Facade 对业务暴露不可变 Kotlin data class、sealed error 和 ByteArray Cursor。 close() 最终释放 native handle,但正常退出先执行 shutdown()。
4.1 Easy Login 与凭证轮换
connect 是推荐的业务入口。它先请求公开的 GET /v1/sdk/config,创建 App ID + User ID 哈希隔离的 noBackupFilesDir 数据库,再复用现有 start/login。调用方不再手工复制 Endpoint Key ID 和 Ed25519 公钥。
Development 服务端明确返回 environment=development 且 development_login_enabled=true 时,默认认证会请求 POST /v1/sdk/development:login。Production 使用宿主业务登录态:
kotlin
val client = XHIMClient.connect(
context = applicationContext,
server = BuildConfig.XHIM_SERVER_URL,
userId = session.userId,
authentication = XHIMAuthentication.Business(
XHIMCredentialProvider {
businessAccountApi.fetchXHIMCredential().accessToken
},
),
)Core 进入 CREDENTIAL_REQUIRED 后,SDK 在账号级协程中重新调用同一 Provider 并执行 updateCredential;同一时刻只允许一个续凭任务。失败不会隐藏公开状态, 业务仍可观察事件并决定提示或重新登录。AccessToken 模式只用于固定凭证迁移和 受控测试。
Bootstrap 安全合同:
- Server URL 只接受绝对 HTTP/HTTPS,拒绝 userinfo、query、fragment 和非法端口;
- 配置与登录请求不跟随 3xx,响应流和
Content-Length都限制为 256 KiB; - HTTP 只在配置确认
environment=development后允许继续,其他环境 fail closed; - Development 认证在 test/production 环境一律拒绝;
- APK 不保存或请求 Admin Key、Endpoint 私钥及其他账号 Token;
- Production 只使用业务
XHIMCredentialProvider或显式固定用户 Token; - Provider 只返回凭证字符串,Endpoint 发现和 Core 状态机仍由 SDK 管理。
需要完全手工托管的高级客户仍可使用 XHIMClientConfiguration、 XHIMDeployment、start/login/updateCredential;这不是空白工程的默认路径。
4.2 已读调用
messages 返回最新消息在前的强类型页面;nextCursor 是可能包含 NUL 的二进制 游标,只能复制并原样传入下一次同账号、同会话查询。在聊天列表已经展示一批 服务端确认消息后,提交其中最大的正 serverSequence:
kotlin
val page = client.messages(conversationId)
val highestVisibleServerSequence = page.messages
.mapNotNull { it.serverSequence }
.maxOrNull()
?: return
val receipt = client.markConversationRead(
conversationId = conversationId,
throughServerSequence = highestVisibleServerSequence,
)
// 以 receipt.unreadCount 刷新 UI。不要提交 Pending/Sending 消息,不要在挂起函数完成前乐观清零。收到 XHIMEvent.ConversationReadChanged(conversationId) 后重新加载该会话摘要; 该事件也用于同账号其他设备已读后的失效通知。同值或更低值是幂等 no-op。
conversations 返回置顶优先、随后按持久活动排序的摘要页。业务不得通过 JNI 私有入口、SQLite、列表下标或设备时间生成服务端序号。
实时 Presence/Typing(非持久投影)
publishPresence/publishTyping 只使用当前已登录会话,业务层不传 Token。 权威回显和对端 PresenceChanged/TypingChanged 都携带 sequence 与 expiresAtMilliseconds。这两类状态不落 SQLite,也不属于可重查的投影事件; UI 到期必须清除,并且只上报开始/停止输入转换,禁止逐按键发送。
4.3 自定义消息与 Compose Renderer
每个业务类型注册一个精确 contentType,发送时先校验插件,再提交相同信封:
kotlin
val plugins = XHIMMessagePluginRegistry().apply {
register(ProductCardPlugin())
}
val outgoing = XHIMOutgoingMessage(
contentType = "com.xihan.product-card",
contentVersion = 1,
payload = cardJson.encodeToByteArray(),
fallbackText = "[商品卡片]",
)
plugins.validate(outgoing)
client.sendMessage(conversationId, outgoing)收到或分页读取消息后调用 plugins.presentation(message)。插件未注册、不支持 更高版本、校验失败或抛异常时,Registry 使用 fallbackText 生成安全的会话/ 通知摘要;Core 仍保存原始 ByteArray,Cursor 可继续推进。
Compose 层可把 rendererKey 注册到 XHIMMessageRendererRegistry 的 XHIMMessageItemRenderer。Renderer 只处理复制后的平台消息模型;异常会退回 基础 MessageItem,不得直接调用 JNI 或读取 SQLite。可运行入口见 sample/.../QuickStart.kt。
5. JNI Bridge 规则
JNI_OnLoad中保存JavaVM*并使用RegisterNatives;不要依赖易被混淆或 重命名破坏的长 JNI 符号;JNIEnv*只属于当前线程,不能缓存后跨线程使用;native Callback 线程需要 按 JVM 合同 attach/detach;- Kotlin/Java callback owner 使用受控 global reference,并在 subscription 取消或最终请求完成后准确释放;
- C Callback 内先复制 byte view 和数组,再投递到 Kotlin dispatcher;
- UTF-8 bytes 与 JVM UTF-16 String 显式转换,不能使用字符数代替字节数;
- 同一个 native request 只恢复一次 continuation;同步拒绝必须立即清理引用;
- 协程取消会用 JNI 返回的 request ID 调用 native
cancelRequest;这只取消 Completion,不回滚已经提交的消息/已读事务; XHIMException完整复制domain/stableCode/nativeCode/retryable/retryAfterMilliseconds/userAction/operationId/traceId,业务不解析 message;CREDENTIAL_REQUIRED后只调用updateCredential;不要销毁 profile 或替换 Core 持有的 canonical account;cancelMessage只取消尚未被 Transport 获取的本地 Outbox,不等于服务端撤回;- 已读 Bridge 必须复制 typed receipt,并把会话 ID 作为变更事件;Kotlin 不 维护独立 read sequence,也不在服务端确认前清零;
client_destroy与全部 JNI Client 调用串行化;Cleaner 只能做泄漏兜底;- native 异常不能穿过 JNI;C ABI status/error 转为稳定 Kotlin 错误。
R8/ProGuard 规则只保留 JNI 注册和序列化确实需要的类/成员,不使用一条全包 -keep 掩盖错误。release minify 和 debug 都必须跑消费测试。
6. 生命周期和进程模型
Client 由 Application/账号级容器拥有,不由 Activity、Fragment 或 Compose 页面拥有。配置变化、页面重建不能重复创建 Client。
text
Application 进程启动 → create/start
账号登录 → login,等待 Ready
Activity 重建 → 复用同一 Client
账号退出 → logout(清除自动续凭 Provider)
进程受控关闭 → shutdown/close当前版本不支持多个 Android 进程同时打开一个 XHIM profile。如果 Push、Service 或独立进程需要 IM 数据,必须通过单一进程 Service/IPC 设计,而不是各自创建 Client。WorkManager 不能用来维持永久 WebSocket;后台策略必须符合系统限制。
7. 存储和安全
- 数据库放在
filesDir或经产品确认的noBackupFilesDir账号隔离子目录, 不放cacheDir、外置存储或公共 Downloads; - Cursor 使用
ByteArray原样保存,不转 String/Base64 后再作为业务语义; - Token、数据库密钥和媒体密钥进入 Android Keystore 支持的安全封装;由于 Keystore AES Key 不可导出,使用它包装随机 SQLCipher 数据库密钥;
- 日志和 Crash 不记录 Token、消息正文、完整 Cursor、临时 URL 或本地路径;
- 清理账号数据前必须先 logout/shutdown 并关闭数据库;
- 备份/换机策略必须明确:是否同步数据库、密钥是否可恢复、恢复失败如何重建。
8. 附件消息与可选实时音视频
XHIMAttachmentPicker使用 Activity Result、Photo Picker、OpenDocument 和 宿主 FileProvider 相机 URI;content://授权只在上传所需生命周期内持有;sendImage/sendVideo/sendAudio/sendFile的File重载默认创建 Core 持久化上传任务;Hash、鉴权、传输、校验、恢复和依赖消息入队由 Core 负责;- API 返回只表示 durable admission;媒体事件仅提供 task hint,完整状态通过
mediaTask()查询;cancelMediaTask()与协程触发的通用cancelRequest语义不同; acceptMediaDownload()使用稳定MediaRef和扁平cacheKey创建私有 缓存下载;不得从 storage path 推导缓存路径;- 完成后只通过
XHIMMediaCacheReader的read/chunks消费已校验字节; open/read 在 Dispatchers.IO,单 Reader 串行,停止 Flow 即取消; - Product Adapter 未提供 no-follow 安全 reader 时,open 明确映射
media_cache_unsupported(103),不退化为 Java 路径访问; - task、event、
diagnostics()和网络 Payload 都不含本地路径、Token、临时 URL 或签名 URL;diagnostics 是同步、只读、纯内存快照; ConnectivityManager报告网络恢复时调用notifyNetworkAvailable(); 该同步提示没有 request ID,既有 Core 重连定时器仍是保底;XHIMServerMediaUploader/XHIMMediaUploader仅保留为旧版兼容迁移路径;- JNI 不把 Java InputStream 或文件描述符长期悬挂在 C++ 任务里;
- 实时音视频不阻塞纯 IM 首发;以后以独立
xhim-call包接入厂商 Provider; - AudioFocus、Bluetooth、前台 Service、Telecom/ConnectionService 和厂商 Java/C++ 对象只存在于可选通话 Adapter,不进入消息 Payload 或稳定 C ABI。
9. UI Kit 与二次开发
建议模块:
text
xhim-sdk Headless
xhim-ui-common 主题、资源、Renderer SPI
xhim-ui-compose Compose 页面
xhim-ui-view View/Fragment 页面
xhim-call RTC 与系统通话桥UI Kit 只依赖 Kotlin Facade。自定义消息通过 Renderer registry 注入,主题、头像 加载、路由、菜单、通知摘要和国际化均使用显式接口,不允许 UI 层直接 JNI 或 读 SQLite。
xhim-ui-compose 已把同一套 Lucide SVG 转换成 VectorDrawable,通过 XHIMIcon(XHIMIcon.Send, ...) 使用;XHIMTheme 提供默认明暗色,颜色和字体 仍可由宿主 Material 3 MaterialTheme 覆盖。资源版本、SHA-256 和许可证见 五端 UI 资源说明。
10. 发布验收
- 空白 Kotlin App 只通过 Maven 包完成 debug/release 和 minify 构建;
- arm64 真机、x86_64 Emulator,以及声明支持的其他 ABI 全部验证;
libxhim_jni.so、C++ runtime 和所有依赖没有重复/缺失;- StrictMode、ASan/HWASan(适用设备)、线程与 JNI 引用压力测试通过;
- Activity 重建、后台限制、进程重启、网络切换、账号切换无双 Client;
- AAR 包含 consumer rules、LICENSE、NOTICE、版本清单、可关联的 native Build ID、Hash 和 SBOM;未剥离符号只进入受控内部符号服务;
- 删除 UI artifact 后 Headless SDK 仍可完整使用。
11. OfflineReader 平台契约
XHIMOfflineReader 直接桥接只读 C ABI,并复用正式 QueryCodec 和公开模型, 覆盖消息、搜索、会话与六类社交分页。
- Kotlin 通过
Dispatchers.IO执行同步 JNI/SQLite 调用,以Mutex串行同一 native handle; - JNI 在调用返回前编码并复制所有 borrowed view,Kotlin 不保存 native 指针;
close()为 suspend、幂等,关闭后的操作映射为带原始 code 的XHIMException;- Key 通过
XHIMOfflineDatabaseKeyProvider注入。SDK 对 Provider 返回值再做 临时副本并在 open 后清零;Provider/安全存储负责其原始数组生命周期; - 未知 status 和未知枚举原值可诊断,opaque cursor 以
ByteArray原样往返。
接入示例见 Android 快速接入。禁止在 resources、BuildConfig、SharedPreferences、日志或测试 fixture 中保存正式 数据库 Key。