# XuqmGroup Android SDK 文档 > Kotlin 2.3.10 · AGP 9.1.0 · minSdk 24 · compileSdk 36 ## 模块结构 ``` XuqmGroup-AndroidSDK/ ├── sdk-core/ # 核心:初始化、基础 HTTP、本地文件、时间、安全存储 ├── sdk-bugcollect/ # 日志监控:错误上报、Crash 捕获、漏斗分析(含 Gradle Plugin) ├── sdk-im/ # IM:WebSocket 实时通信 ├── sdk-push/ # 推送:设备 Token 注册 ├── sdk-update/ # 版本管理:检查更新、下载安装 ├── sdk-webview/ # WebView:嵌入式组件 / 独立页面 └── sample-app/ # 示例 App(Jetpack Compose) ``` ## 集成 ### Gradle(Maven 私有仓库) 在项目根 `settings.gradle.kts` 添加仓库: ```kotlin dependencyResolutionManagement { repositories { maven { url = uri("https://nexus.xuqinmin.com/repository/android-hosted/") credentials { username = providers.gradleProperty("NEXUS_USER").orNull ?: "" password = providers.gradleProperty("NEXUS_PASSWORD").orNull ?: "" } } } } ``` 所有宿主变体必须应用 common 构建插件。它会扫描宿主 `src/**`,只接受唯一固定路径 `src/main/assets/config/config.xuqmconfig`;任何改名、嵌套或其它 sourceSet 中的 `.xuqmconfig` 文件都会直接失败。 Debug 执行本地 V2 验签,Release 额外执行租户平台在线有效性校验,并负责不可变 buildId 和 BugCollect 构建门禁,并生成扩展自动初始化所需的唯一 Manifest 标记;BugCollect 插件 仅负责 R8 mapping 上传。Debug 自动采集固定关闭,但非应急禁用的可调试宿主可通过内部 开发入口显式发送一次测试事件。 ```kotlin plugins { id("com.xuqm.common") version "VERSION" // 使用 BugCollect 且 Release 开启 R8 时再添加: id("com.xuqm.bugcollect") version "VERSION" } ``` 在 `gradle.properties` 或环境变量中配置: ```properties NEXUS_USER=your_username NEXUS_PASSWORD=your_password ``` 引入依赖: ```kotlin dependencies { implementation("com.xuqm:sdk-core:VERSION") implementation("com.xuqm:sdk-bugcollect:VERSION") // 可选 implementation("com.xuqm:sdk-im:VERSION") // 可选 implementation("com.xuqm:sdk-push:VERSION") // 可选 implementation("com.xuqm:sdk-update:VERSION") // 可选 implementation("com.xuqm:sdk-webview:VERSION") // 可选 } ``` ## 快速开始 ### 1. 初始化 从租户平台下载签名的 `config.xuqmconfig`,原样放入 `app/src/main/assets/config/config.xuqmconfig`。ContentProvider 会自动初始化,宿主无需 编写初始化代码,也不得修改、复制、自行生成或在 flavor/buildType 下放置第二份文件。 配置中的 `serverUrl` 是唯一平台地址且必填,运行时没有默认地址或跨平台回退。 仅依赖 `sdk-core` 时不会合并初始化 Provider或通知权限,也不要求配置文件、初始化或 登录;core 只注册安全打开/分享本地文件所需的专用 FileProvider。引入任一需要平台 能力的扩展后,各 AAR 中同名声明会合并为唯一 `XuqmMergedProvider`;Provider 只有检测到 common 构建插件生成的标记后才会自动读取 上述配置,避免未应用插件的宿主发生隐式初始化。 ### 2. 用户登录后初始化 IM ```kotlin // 调用业务登录接口,拿到 userSig 后只需要登录一次 SDK val userSig = api.getUserSig(userId) XuqmSDK.setUserInfo(XuqmUserInfo(userId = userId, userSig = userSig)) // 如果工程里集成了 sdk-im / sdk-push / sdk-update, // SDK 会自动完成对应模块的登录与初始化 ``` ### 3. WebView 独立模块 `sdk-webview` 不需要额外初始化,直接依赖 `sdk-core` 即可使用。 ```kotlin import com.xuqm.sdk.webview.XWebViewConfig import com.xuqm.sdk.webview.XWebViewView import com.xuqm.sdk.webview.XWebViewScreen import com.xuqm.sdk.webview.openXWebView // 嵌入式组件:直接放到页面中,不包含导航栏 / 状态栏 XWebViewView( config = XWebViewConfig( url = "https://example.com", title = "嵌入式网页", ), ) // 独立页面:先设置配置,再跳转到页面 openXWebView( XWebViewConfig( url = "https://example.com", title = "独立页面", ) ) // navigate("xwebview") 后使用 XWebViewScreen() ``` ## sdk-core ### SDKConfig | 参数 | 类型 | 说明 | |------|------|------| | `appKey` | String | 应用标识(租户平台获取) | | `logLevel` | LogLevel | 日志等级 | ### TokenStore 基于统一的 `SecureStore` 持久化:密钥由 Android Keystore 生成且不可导出, 值使用 AES-256-GCM 加密并通过 namespace/key 关联数据认证。 ## Sample Release 签名 示例应用的 Release 签名只从用户级 `~/.gradle/gradle.properties` 读取,仓库不保存 密钥库路径或凭据。需要配置的属性名为: - `YIWANGXIN_STORE_FILE` - `YIWANGXIN_STORE_PASSWORD` - `YIWANGXIN_ALIAS` - `YIWANGXIN_KEY_PASSWORD` 缺少属性或密钥库文件不可读时,Release 构建会在签名前失败;Debug 使用 Android 默认调试签名,不读取生产签名材料。 Sample 还要求把租户平台签发且包名匹配的文件放到 `sample-app/src/main/assets/config/config.xuqmconfig`。该文件被 Git 忽略;Debug 与 Release 构建都会在文件缺失时给出明确错误,禁止提交、伪造或手工修改配置。 运行 Sample 仪器集成测试时,测试密码通过 `-e XUQM_TEST_PASSWORD ` 注入,禁止写入源码或 Gradle 配置。 ```kotlin // 存储 XuqmSDK.tokenStore.saveToken("eyJ...") // 读取(协程) val token = XuqmSDK.tokenStore.getToken() // 清除(登出) XuqmSDK.tokenStore.clear() ``` ### 网络边界 `sdk-core` 的 `CommonHttpClient`、下载、摘要、打开和分享能力无需 SDK 初始化; `FileSDK` 的上传入口位于同一 core,但只有上传会等待平台配置和登录态。 ### FileSDK 下载、摘要、平台上传与 FileProvider 安全打开统一由 `com.xuqm.sdk.file.FileSDK` 提供。 #### 上传 ```kotlin // 从 Uri(文件选择器返回值)上传,自动解析文件名和 MIME 类型 val result: FileUploadResult = FileSDK.upload( context = context, uri = uri, onProgress = { progress -> /* 0–100 */ }, ) // 直接上传字节数组(如相机拍照后的 ByteArray) val result = FileSDK.uploadBytes( fileName = "photo.jpg", mimeType = "image/jpeg", bytes = byteArray, onProgress = { progress -> }, ) // 上传 File 对象 val result = FileSDK.upload(file = file) ``` `FileUploadResult` 字段:`url`、`thumbnailUrl`、`hash`、`size`、`originalName`、`mimeType`、`ext` 。上传入口会自动等待平台初始化;服务端业务失败或响应结构错误统一抛出保留 `code/status/message` 的 `FileUploadException`,不会暴露原始响应体。 #### 下载 ```kotlin // 下载存储目标 sealed class FileDownloadDestination { data object Sandbox : FileDownloadDestination() // 应用私有目录(无需权限) data object PublicDownloads : FileDownloadDestination() // 系统 Downloads 文件夹 } // 下载到指定目标,支持通知栏进度 val file: File = FileSDK.download( context = context, downloadUrl = "https://example.com/report.pdf", fileName = "report.pdf", // 可选,默认从 URL 推断 destination = FileDownloadDestination.PublicDownloads, notificationTitle = "正在下载", // 非 null 时显示通知栏进度条 onProgress = { progress -> /* 0–100,同步进度到 H5 或 UI */ }, ) ``` > **通知栏进度**:设置 `notificationTitle` 后,下载过程中通知栏会显示带进度条的持续通知,完成后自动切换为完成图标。需在 `AndroidManifest.xml` 中声明 `POST_NOTIFICATIONS` 权限(Android 13+)。 #### 打开文件 ```kotlin // 用系统应用打开本地文件(通过 FileProvider + ACTION_VIEW) FileSDK.openFile(context, file) ``` `openFile` 是纯本地操作,不要求 SDK 初始化。 `sdk-core` 是该 FileProvider、文件下载、上传、打开和分享能力的唯一所有者; `sdk-update`、`sdk-im` 和 `sdk-webview` 均直接复用 core,不再发布独立 `sdk-file` 制品。 --- ## sdk-im ### Android sample 已具备 - 会话列表先读本地缓存,再刷新网络 - 联系人列表先读本地缓存,再刷新网络 - 聊天历史分页加载 - 当前会话本地搜索 - 输入草稿自动保存 - 群设置支持编辑群名和群公告 - 群组扩展 API 已补齐:管理员设置、禁言、解散群 - 公开群支持搜索、创建和加群审批 - 会话支持本地删除 - 显示总未读数 - 消息状态直接展示 - 会话置顶/免打扰/已读/草稿/删除同步服务端 - IM 连接状态提示 - SDK 登录态恢复后自动重连 - 重复调用登录会覆盖当前 IM 会话并自动重连,SDK 侧不做生命周期检测或维护 - 群聊支持 `@userId` 提及,并写入 `mentionedUserIds` - 关系链支持好友申请、接受/拒绝、黑名单 - 支持图片 / 视频 / 音频 / 文件消息,文件通过独立文件服务上传后再发 IM - 图片支持拍照、摄像、图库选择,文件支持文件管理器选择 - 语音支持长按录音、抬手发送 - 支持引用回复、群聊已读人数展示 - 支持 `LOCATION` / `CUSTOM` / `RICH_TEXT` / `FORWARD` / `QUOTE` / `MERGE` / `CALL_AUDIO` / `CALL_VIDEO` 等通用消息类型发送 - 单聊支持已读回执,服务端会把 `READ` 状态推回发送者 - `sdk-webview` 作为独立模块提供嵌入式组件和独立页面两种形态,可与 IM / Push / Update 任意组合 ### ImClient ```kotlin val imClient = ImClient() // 注册事件监听 imClient.listener = object : ImEventListener { override fun onConnected() { /* WebSocket 连接成功 */ } override fun onDisconnected(code: Int, reason: String) { /* 断开 */ } override fun onMessage(msg: ImMessage) { /* 收到新消息 */ } override fun onRevoke(msgId: String, operatorId: String) { /* 消息被撤回 */ } override fun onError(t: Throwable) { /* 连接错误 */ } } // 连接 imClient.connect() // 发送消息 imClient.send(SendMessageParams( toId = "user_002", chatType = ChatType.SINGLE, msgType = MsgType.TEXT, content = "Hello!" )) // 群聊提及 imClient.send(SendMessageParams( toId = "group_001", chatType = ChatType.GROUP, msgType = MsgType.TEXT, content = "@user_002 你好", mentionedUserIds = "user_002", )) // 撤回消息 imClient.revoke(msgId = "uuid") // 断开连接(Activity/Fragment 销毁时调用) imClient.disconnect() ``` ### 消息类型(MsgType) `TEXT` / `IMAGE` / `VIDEO` / `AUDIO` / `FILE` / `CUSTOM` / `LOCATION` / `NOTIFY` / `RICH_TEXT` / `CALL_AUDIO` / `CALL_VIDEO` / `FORWARD` / `QUOTE` / `MERGE` ### ImMessage 结构 ```kotlin data class ImMessage( val id: String, val fromId: String, val toId: String, val chatType: ChatType, // SINGLE / GROUP val msgType: MsgType, val content: String, val extra: String?, val revoked: Boolean, val createdAt: String ) ``` ### 自动重连 断线后指数退避重连,初始间隔 3 秒,最大间隔 30 秒。调用 `disconnect()` 后停止重连。 --- ## sdk-push ### 推送接入 宿主调用 `XuqmSDK.setUserInfo(XuqmUserInfo(...))` 后,SDK 会自动完成当前系统推送 token 的注册与上传,不再要求业务侧手工传入 token;传入 `null` 时自动注销当前设备绑定。 还可以按用户设置接收开关: ```kotlin PushSDK.setReceivePush(context, enabled = false) PushSDK.setReceivePush(context, enabled = true) ``` 如果项目接入了 Firebase Messaging,`sdk-push` 会通过 `FirebaseMessagingService.onNewToken()` 自动接收并上报 FCM token。对应服务已经随库注册,只要应用工程提供 Firebase 配置即可生效。 ### 与 IM 联动 当 IM 和推送服务均已开启时,IM 服务在目标用户离线时会自动调用推送服务发送离线推送通知。 业务方只需分别注册推送 Token 和 IM 登录,无需额外配置。 --- ## sdk-update ### 检查原生 App 更新 ```kotlin val result: UpdateResult = UpdateSDK.checkUpdate( appKey = "ak_xxx", platform = Platform.ANDROID ) if (result.needsUpdate) { // result.versionName, result.downloadUrl, result.forceUpdate UpdateSDK.downloadAndInstall(context, result.downloadUrl) } ``` `downloadAndInstall` 会将 APK 下载到 `getExternalFilesDir(null)`,通过 `FileProvider` 触发系统安装。 AndroidManifest 中已配置 `@xml/file_paths`(`external-files-path`)。 --- ## sdk-webview `XWebViewView` 是基于 `android.webkit.WebView` 封装的 Jetpack Compose 组件,内置文件选择、拍照、下载拦截和 JS 通信能力。 ### XWebViewConfig 完整参数 | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `url` | String | `""` | 初始加载地址 | | `title` | String | `""` | 页面标题(独立页面模式使用) | | `hideToolbar` | Boolean | `false` | 隐藏独立页面顶栏 | | `hideStatusBar` | Boolean | `false` | 隐藏状态栏 | | `userAgent` | String? | `null` | 自定义 User-Agent | | `injectedJavaScript` | String? | `null` | 页面加载后注入的额外 JS | | `jsBridgeName` | String | `"XWebViewBridge"` | JS 桥接对象名 | | `debugEnabled` | Boolean | `false` | 开启 WebView 远程调试 | | `downloadDestination` | FileDownloadDestination | `Sandbox` | 下载文件存储目标 | | `downloadNotificationTitle` | String? | `null` | 非 null 时通知栏显示下载进度 | | `onMessage` | `(String) -> Unit`? | `null` | H5 发送消息的回调 | ### 文件选择与拍照 WebView 内 `` 和 `` 均已内置处理: - `accept="image/*"` + `capture` → 调起系统相机,自动申请 `CAMERA` 权限 - `accept=".docx,.xlsx"` 等扩展名格式 → 自动映射为正确的 MIME 类型后调起文件选择器 - `getUserMedia()` WebRTC 摄像头 → 自动请求 `CAMERA` 权限后授权 ### 下载拦截 注入的 JS 自动拦截以下两种场景,下载完成后调用 `FileSDK.openFile()` 打开文件: - 带 `download` 属性的 `` 标签,或链接以可下载扩展名(`.pdf`、`.zip`、`.docx` 等)结尾 - Blob URL(自动转 base64 后传给 native 处理) 公共 Downloads 复用 `FileSDK` 的 MediaStore 实现,不申请 `MANAGE_EXTERNAL_STORAGE`,也不会跳转“所有文件访问权限”设置页。Android 9 及以下 仍按系统规范处理 `WRITE_EXTERNAL_STORAGE` 运行时权限。 ```kotlin XWebViewView( config = XWebViewConfig( url = "https://example.com", downloadDestination = FileDownloadDestination.PublicDownloads, // 存入系统 Downloads downloadNotificationTitle = "正在下载", // 通知栏进度 ) ) ``` ### H5 监听下载进度 H5 页面可通过 `window.addEventListener` 接收下载进度和完成事件: ```javascript // 下载进度(0–100) window.addEventListener('__xwvDownloadProgress', (e) => { console.log(e.detail.url, e.detail.progress) }) // 下载完成 window.addEventListener('__xwvDownloadDone', (e) => { if (e.detail.success) { console.log('下载成功', e.detail.url) } else { console.error('下载失败', e.detail.error) } }) ``` ### H5 ↔ Native 消息通信 ```javascript // H5 发消息给 Native(触发 onMessage 回调) window.XWebViewBridge.postMessage(JSON.stringify({ type: 'login', token: '...' })) ``` ```kotlin XWebViewConfig( onMessage = { raw -> val json = JSONObject(raw) when (json.optString("type")) { "login" -> { /* 处理登录 */ } } } ) ``` ```kotlin // Native 发消息给 H5 val controller = getXWebViewController() controller?.postMessageToWeb("window.dispatchEvent(new CustomEvent('nativeMsg', { detail: { key: 'value' } }))") ``` --- ## 发版 ```bash # 在 gradle.properties 中配置 NEXUS_USER / NEXUS_PASSWORD ./gradlew :sdk-core:publish ./gradlew :sdk-im:publish ./gradlew :sdk-push:publish ./gradlew :sdk-update:publish ``` 发布至 `https://nexus.xuqinmin.com/repository/android-hosted/`,groupId `com.xuqm`,版本号在各模块 `build.gradle.kts` 中维护。 ### Jenkins SSH Windows Jenkins 节点通过 `jenkins-ssh-key` 私钥凭据生成构建期临时 `git-ssh.cmd` 执行版本提交推送,作用域结束后立即删除该脚本。