refactor(file): consolidate file capabilities into core
这个提交包含在:
父节点
8610ea5745
当前提交
b34b88e29e
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@ -12,7 +12,6 @@ pipeline {
|
|||||||
|
|
||||||
// 要构建并发布的模块(勾选即发布)
|
// 要构建并发布的模块(勾选即发布)
|
||||||
booleanParam(name: 'MOD_CORE', defaultValue: false, description: '发布 sdk-core')
|
booleanParam(name: 'MOD_CORE', defaultValue: false, description: '发布 sdk-core')
|
||||||
booleanParam(name: 'MOD_FILE', defaultValue: false, description: '发布 sdk-file')
|
|
||||||
booleanParam(name: 'MOD_IM', defaultValue: false, description: '发布 sdk-im')
|
booleanParam(name: 'MOD_IM', defaultValue: false, description: '发布 sdk-im')
|
||||||
booleanParam(name: 'MOD_PUSH', defaultValue: false, description: '发布 sdk-push')
|
booleanParam(name: 'MOD_PUSH', defaultValue: false, description: '发布 sdk-push')
|
||||||
booleanParam(name: 'MOD_UPDATE', defaultValue: false, description: '发布 sdk-update')
|
booleanParam(name: 'MOD_UPDATE', defaultValue: false, description: '发布 sdk-update')
|
||||||
@ -37,7 +36,6 @@ pipeline {
|
|||||||
def moduleChecks = [
|
def moduleChecks = [
|
||||||
'sdk-common-plugin': params.MOD_CORE,
|
'sdk-common-plugin': params.MOD_CORE,
|
||||||
'sdk-core': params.MOD_CORE,
|
'sdk-core': params.MOD_CORE,
|
||||||
'sdk-file': params.MOD_FILE,
|
|
||||||
'sdk-im': params.MOD_IM,
|
'sdk-im': params.MOD_IM,
|
||||||
'sdk-push': params.MOD_PUSH,
|
'sdk-push': params.MOD_PUSH,
|
||||||
'sdk-update': params.MOD_UPDATE,
|
'sdk-update': params.MOD_UPDATE,
|
||||||
|
|||||||
33
README.md
33
README.md
@ -7,7 +7,6 @@
|
|||||||
```
|
```
|
||||||
XuqmGroup-AndroidSDK/
|
XuqmGroup-AndroidSDK/
|
||||||
├── sdk-core/ # 核心:初始化、基础 HTTP、本地文件、时间、安全存储
|
├── sdk-core/ # 核心:初始化、基础 HTTP、本地文件、时间、安全存储
|
||||||
├── sdk-file/ # 平台文件上传与 FileProvider 打开能力
|
|
||||||
├── sdk-bugcollect/ # 日志监控:错误上报、Crash 捕获、漏斗分析(含 Gradle Plugin)
|
├── sdk-bugcollect/ # 日志监控:错误上报、Crash 捕获、漏斗分析(含 Gradle Plugin)
|
||||||
├── sdk-im/ # IM:WebSocket 实时通信
|
├── sdk-im/ # IM:WebSocket 实时通信
|
||||||
├── sdk-push/ # 推送:设备 Token 注册
|
├── sdk-push/ # 推送:设备 Token 注册
|
||||||
@ -61,7 +60,6 @@ NEXUS_PASSWORD=your_password
|
|||||||
```kotlin
|
```kotlin
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.xuqm:sdk-core:VERSION")
|
implementation("com.xuqm:sdk-core:VERSION")
|
||||||
implementation("com.xuqm:sdk-file:VERSION") // 平台文件能力可选
|
|
||||||
implementation("com.xuqm:sdk-bugcollect:VERSION") // 可选
|
implementation("com.xuqm:sdk-bugcollect:VERSION") // 可选
|
||||||
implementation("com.xuqm:sdk-im:VERSION") // 可选
|
implementation("com.xuqm:sdk-im:VERSION") // 可选
|
||||||
implementation("com.xuqm:sdk-push:VERSION") // 可选
|
implementation("com.xuqm:sdk-push:VERSION") // 可选
|
||||||
@ -79,8 +77,9 @@ dependencies {
|
|||||||
编写初始化代码,也不得修改、复制、自行生成或在 flavor/buildType 下放置第二份文件。
|
编写初始化代码,也不得修改、复制、自行生成或在 flavor/buildType 下放置第二份文件。
|
||||||
配置中的 `serverUrl` 是唯一平台地址且必填,运行时没有默认地址或跨平台回退。
|
配置中的 `serverUrl` 是唯一平台地址且必填,运行时没有默认地址或跨平台回退。
|
||||||
|
|
||||||
仅依赖 `sdk-core` 时不会合并初始化 Provider、通知权限或 FileProvider,也不要求配置
|
仅依赖 `sdk-core` 时不会合并初始化 Provider或通知权限,也不要求配置文件、初始化或
|
||||||
文件、初始化或登录。引入任一需要平台能力的扩展后,各 AAR 中同名声明会合并为唯一
|
登录;core 只注册安全打开/分享本地文件所需的专用 FileProvider。引入任一需要平台
|
||||||
|
能力的扩展后,各 AAR 中同名声明会合并为唯一
|
||||||
`XuqmMergedProvider`;Provider 只有检测到 common 构建插件生成的标记后才会自动读取
|
`XuqmMergedProvider`;Provider 只有检测到 common 构建插件生成的标记后才会自动读取
|
||||||
上述配置,避免未应用插件的宿主发生隐式初始化。
|
上述配置,避免未应用插件的宿主发生隐式初始化。
|
||||||
|
|
||||||
@ -170,26 +169,26 @@ XuqmSDK.tokenStore.clear()
|
|||||||
|
|
||||||
### 网络边界
|
### 网络边界
|
||||||
|
|
||||||
`sdk-core` 的 `CommonHttpClient` 与下载/摘要能力无需 SDK 初始化;依赖租户平台认证、
|
`sdk-core` 的 `CommonHttpClient`、下载、摘要、打开和分享能力无需 SDK 初始化;
|
||||||
服务地址和 FileProvider 的上传、打开能力统一由 `sdk-file` 的 `PlatformFileSDK` 提供。
|
`FileSDK` 的上传入口位于同一 core,但只有上传会等待平台配置和登录态。
|
||||||
|
|
||||||
### FileSDK
|
### FileSDK
|
||||||
|
|
||||||
纯本地能力位于 `com.xuqm.sdk.file.FileSDK`;平台上传与 FileProvider 打开位于
|
下载、摘要、平台上传与 FileProvider 安全打开统一由
|
||||||
`com.xuqm.sdk.file.platform.PlatformFileSDK`。
|
`com.xuqm.sdk.file.FileSDK` 提供。
|
||||||
|
|
||||||
#### 上传
|
#### 上传
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// 从 Uri(文件选择器返回值)上传,自动解析文件名和 MIME 类型
|
// 从 Uri(文件选择器返回值)上传,自动解析文件名和 MIME 类型
|
||||||
val result: FileUploadResult = PlatformFileSDK.upload(
|
val result: FileUploadResult = FileSDK.upload(
|
||||||
context = context,
|
context = context,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
onProgress = { progress -> /* 0–100 */ },
|
onProgress = { progress -> /* 0–100 */ },
|
||||||
)
|
)
|
||||||
|
|
||||||
// 直接上传字节数组(如相机拍照后的 ByteArray)
|
// 直接上传字节数组(如相机拍照后的 ByteArray)
|
||||||
val result = PlatformFileSDK.uploadBytes(
|
val result = FileSDK.uploadBytes(
|
||||||
fileName = "photo.jpg",
|
fileName = "photo.jpg",
|
||||||
mimeType = "image/jpeg",
|
mimeType = "image/jpeg",
|
||||||
bytes = byteArray,
|
bytes = byteArray,
|
||||||
@ -197,12 +196,12 @@ val result = PlatformFileSDK.uploadBytes(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 上传 File 对象
|
// 上传 File 对象
|
||||||
val result = PlatformFileSDK.upload(file = file)
|
val result = FileSDK.upload(file = file)
|
||||||
```
|
```
|
||||||
|
|
||||||
`FileUploadResult` 字段:`url`、`thumbnailUrl`、`hash`、`size`、`originalName`、`mimeType`、`ext`
|
`FileUploadResult` 字段:`url`、`thumbnailUrl`、`hash`、`size`、`originalName`、`mimeType`、`ext`
|
||||||
。上传入口会自动等待平台初始化;服务端业务失败或响应结构错误统一抛出保留
|
。上传入口会自动等待平台初始化;服务端业务失败或响应结构错误统一抛出保留
|
||||||
`code/status/message` 的 `PlatformFileException`,不会暴露原始响应体。
|
`code/status/message` 的 `FileUploadException`,不会暴露原始响应体。
|
||||||
|
|
||||||
#### 下载
|
#### 下载
|
||||||
|
|
||||||
@ -230,14 +229,14 @@ val file: File = FileSDK.download(
|
|||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// 用系统应用打开本地文件(通过 FileProvider + ACTION_VIEW)
|
// 用系统应用打开本地文件(通过 FileProvider + ACTION_VIEW)
|
||||||
PlatformFileSDK.openFile(context, file)
|
FileSDK.openFile(context, file)
|
||||||
```
|
```
|
||||||
|
|
||||||
`openFile` 是纯本地操作,不要求 SDK 初始化。
|
`openFile` 是纯本地操作,不要求 SDK 初始化。
|
||||||
|
|
||||||
`sdk-file` 是该 FileProvider 的唯一所有者;`sdk-update`、`sdk-im` 和 `sdk-webview`
|
`sdk-core` 是该 FileProvider、文件下载、上传、打开和分享能力的唯一所有者;
|
||||||
均复用它。只集成 `sdk-core` 不会向宿主注入 Provider,也不存在 core 内的上传或打开
|
`sdk-update`、`sdk-im` 和 `sdk-webview` 均直接复用 core,不再发布独立 `sdk-file`
|
||||||
转发 API。
|
制品。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -411,7 +410,7 @@ WebView 内 `<input type="file">` 和 `<input type="file" capture>` 均已内置
|
|||||||
|
|
||||||
### 下载拦截
|
### 下载拦截
|
||||||
|
|
||||||
注入的 JS 自动拦截以下两种场景,下载完成后调用 `PlatformFileSDK.openFile()` 打开文件:
|
注入的 JS 自动拦截以下两种场景,下载完成后调用 `FileSDK.openFile()` 打开文件:
|
||||||
|
|
||||||
- 带 `download` 属性的 `<a>` 标签,或链接以可下载扩展名(`.pdf`、`.zip`、`.docx` 等)结尾
|
- 带 `download` 属性的 `<a>` 标签,或链接以可下载扩展名(`.pdf`、`.zip`、`.docx` 等)结尾
|
||||||
- Blob URL(自动转 base64 后传给 native 处理)
|
- Blob URL(自动转 base64 后传给 native 处理)
|
||||||
|
|||||||
@ -122,10 +122,9 @@ subprojects {
|
|||||||
|
|
||||||
val verifySdkManifestOwnership by tasks.registering {
|
val verifySdkManifestOwnership by tasks.registering {
|
||||||
group = "verification"
|
group = "verification"
|
||||||
description = "验证 common 无宿主 Manifest 副作用,扩展能力各自声明所需组件。"
|
description = "验证 common 的文件共享 Provider 与扩展初始化组件归属。"
|
||||||
dependsOn(
|
dependsOn(
|
||||||
":sdk-core:processDebugManifest",
|
":sdk-core:processDebugManifest",
|
||||||
":sdk-file:processDebugManifest",
|
|
||||||
":sdk-update:processDebugManifest",
|
":sdk-update:processDebugManifest",
|
||||||
":sdk-push:processDebugManifest",
|
":sdk-push:processDebugManifest",
|
||||||
":sdk-im:processDebugManifest",
|
":sdk-im:processDebugManifest",
|
||||||
@ -140,17 +139,13 @@ val verifySdkManifestOwnership by tasks.registering {
|
|||||||
val core = mergedManifest("sdk-core")
|
val core = mergedManifest("sdk-core")
|
||||||
check("XuqmMergedProvider" !in core)
|
check("XuqmMergedProvider" !in core)
|
||||||
check("POST_NOTIFICATIONS" !in core)
|
check("POST_NOTIFICATIONS" !in core)
|
||||||
|
check("com.xuqm.sdk.file.XuqmFileProvider" in core)
|
||||||
check("androidx.core.content.FileProvider" !in core)
|
check("androidx.core.content.FileProvider" !in core)
|
||||||
|
|
||||||
val update = mergedManifest("sdk-update")
|
val update = mergedManifest("sdk-update")
|
||||||
check("XuqmMergedProvider" in update)
|
check("XuqmMergedProvider" in update)
|
||||||
check("androidx.core.content.FileProvider" !in update)
|
check("androidx.core.content.FileProvider" !in update)
|
||||||
|
|
||||||
val file = mergedManifest("sdk-file")
|
|
||||||
check("XuqmMergedProvider" in file)
|
|
||||||
check("com.xuqm.sdk.file.platform.XuqmFileProvider" in file)
|
|
||||||
check("androidx.core.content.FileProvider" !in file)
|
|
||||||
|
|
||||||
val push = mergedManifest("sdk-push")
|
val push = mergedManifest("sdk-push")
|
||||||
check("XuqmMergedProvider" in push)
|
check("XuqmMergedProvider" in push)
|
||||||
check("POST_NOTIFICATIONS" in push)
|
check("POST_NOTIFICATIONS" in push)
|
||||||
@ -172,7 +167,7 @@ val verifySampleManifestMerge by tasks.registering {
|
|||||||
).readText()
|
).readText()
|
||||||
check(Regex("com\\.xuqm\\.sdk\\.internal\\.XuqmMergedProvider")
|
check(Regex("com\\.xuqm\\.sdk\\.internal\\.XuqmMergedProvider")
|
||||||
.findAll(manifest).count() == 1)
|
.findAll(manifest).count() == 1)
|
||||||
check(Regex("com\\.xuqm\\.sdk\\.file\\.platform\\.XuqmFileProvider")
|
check(Regex("com\\.xuqm\\.sdk\\.file\\.XuqmFileProvider")
|
||||||
.findAll(manifest).count() == 1)
|
.findAll(manifest).count() == 1)
|
||||||
check(Regex("com\\.xuqm\\.common_plugin_applied")
|
check(Regex("com\\.xuqm\\.common_plugin_applied")
|
||||||
.findAll(manifest).count() == 1)
|
.findAll(manifest).count() == 1)
|
||||||
|
|||||||
@ -49,26 +49,26 @@ Maven 发布尚未执行。
|
|||||||
:sdk-bugcollect:assembleRelease` 已通过。服务端默认每个 appKey 每分钟最多接收
|
:sdk-bugcollect:assembleRelease` 已通过。服务端默认每个 appKey 每分钟最多接收
|
||||||
6000 个事件,实际部署可通过环境变量调整。
|
6000 个事件,实际部署可通过环境变量调整。
|
||||||
|
|
||||||
|
## 2026-07-28 文件能力归并终态
|
||||||
|
|
||||||
|
- 删除独立 `sdk-file` 模块、版本参数、Jenkins 发布选项和文档入口,不保留空目录或
|
||||||
|
兼容制品。
|
||||||
|
- `sdk-core` 成为下载、摘要、MediaStore、平台上传、FileProvider 安全打开/分享的
|
||||||
|
唯一实现;本地能力和打开文件不要求初始化,只有平台上传等待统一初始化。
|
||||||
|
- `sdk-update`、`sdk-im`、`sdk-webview` 直接依赖 core。专用
|
||||||
|
`XuqmFileProvider` 由 core 唯一声明,仍与宿主自己的 FileProvider 隔离。
|
||||||
|
|
||||||
## 当前目标
|
## 当前目标
|
||||||
|
|
||||||
- `sdk-core` 是配置、会话、网络、文件、时间等公共能力的唯一实现。
|
- `sdk-core` 是配置、会话、网络、文件、时间等公共能力的唯一实现。
|
||||||
- `sdk-update` 只提供更新领域 API;宿主与 RN 桥接不复制下载、校验和安装代码。
|
- `sdk-update` 只提供更新领域 API;宿主与 RN 桥接不复制下载、校验和安装代码。
|
||||||
- APK 更新必须支持 SHA-256、原子临时文件、进度、取消、有限重试、FileProvider、未知来源安装权限及确定错误码。
|
- APK 更新必须支持 SHA-256、原子临时文件、进度、取消、有限重试、FileProvider、未知来源安装权限及确定错误码。
|
||||||
|
|
||||||
## 2026-07-27 sdk-file 与宿主 FileProvider 隔离
|
|
||||||
|
|
||||||
- `sdk-file` 不再直接把 `androidx.core.content.FileProvider` 注册进宿主 Manifest,
|
|
||||||
改为注册唯一的 `XuqmFileProvider` 子类。
|
|
||||||
- Android Manifest 可能按 Provider 类名合并节点;宿主已有 `FileProvider` 时,
|
|
||||||
不同 authority 会在合并阶段冲突。
|
|
||||||
- authority 和公开文件 API 保持不变,宿主不需要 `tools:replace` 或新增 Manifest
|
|
||||||
配置。
|
|
||||||
|
|
||||||
## 2026-07-17 已完成
|
## 2026-07-17 已完成
|
||||||
|
|
||||||
- `FileSDK` 新增详细下载进度、SHA-256 与哈希匹配公共 API。
|
- `FileSDK` 新增详细下载进度、SHA-256 与哈希匹配公共 API。
|
||||||
- 下载改为 `.part` 临时文件完成后替换目标文件;HTTP 非 2xx 失败,协程取消会中断连接并清理临时文件。
|
- 下载改为 `.part` 临时文件完成后替换目标文件;HTTP 非 2xx 失败,协程取消会中断连接并清理临时文件。
|
||||||
- 当时的 core FileProvider 归属已被后续终态替代:当前仅 `sdk-file` 声明,core 零 Manifest。
|
- 当时的 FileProvider 归属已被后续终态替代:当前由 core 唯一声明。
|
||||||
- `sdk-update` 删除无版本 APK 兼容路径和弃用下载入口。
|
- `sdk-update` 删除无版本 APK 兼容路径和弃用下载入口。
|
||||||
- `sdk-update` 2.0.0-SNAPSHOT 强制要求合法 SHA-256,并提供下载重试、取消、缓存文件复验和确定错误码。
|
- `sdk-update` 2.0.0-SNAPSHOT 强制要求合法 SHA-256,并提供下载重试、取消、缓存文件复验和确定错误码。
|
||||||
- 增加 Java/RN 可直接使用的 `startDownloadAndInstall`、`UpdateInstallTask`、进度/结果回调和安装权限设置 Intent。
|
- 增加 Java/RN 可直接使用的 `startDownloadAndInstall`、`UpdateInstallTask`、进度/结果回调和安装权限设置 Intent。
|
||||||
@ -138,15 +138,15 @@ Maven 发布尚未执行。
|
|||||||
`POST /api/sdk/build/config/validate`,提交原始签名配置和最终 applicationId。服务不可达、
|
`POST /api/sdk/build/config/validate`,提交原始签名配置和最终 applicationId。服务不可达、
|
||||||
配置撤销、过期或包名不匹配都会阻止打包;应急关闭 BugCollect 也不能绕过配置校验。
|
配置撤销、过期或包名不匹配都会阻止打包;应急关闭 BugCollect 也不能绕过配置校验。
|
||||||
- 删除不再参与 Manifest 的 `BugCollectInitProvider`,避免扩展模块平行初始化。
|
- 删除不再参与 Manifest 的 `BugCollectInitProvider`,避免扩展模块平行初始化。
|
||||||
- 删除公开手动初始化与旧 Provider 兼容类。`sdk-core` 单独集成不注册 Provider、
|
- 删除公开手动初始化与旧 Provider 兼容类。`sdk-core` 单独集成不注册初始化 Provider
|
||||||
FileProvider 或通知权限;需要平台能力的扩展统一合并同一个 `XuqmMergedProvider`。
|
或通知权限;需要平台能力的扩展统一合并同一个 `XuqmMergedProvider`。文件 Provider
|
||||||
FileProvider 归唯一 `sdk-file`,通知权限归 `sdk-push`。
|
归唯一 core,通知权限归 `sdk-push`。
|
||||||
- Sample 删除硬编码 appKey,只接受被 Git 忽略的租户平台签发配置;缺少文件时 Debug/
|
- Sample 删除硬编码 appKey,只接受被 Git 忽略的租户平台签发配置;缺少文件时 Debug/
|
||||||
Release 构建门禁给出明确错误。Release 签名只从用户级 Gradle 属性读取。
|
Release 构建门禁给出明确错误。Release 签名只从用户级 Gradle 属性读取。
|
||||||
- 敏感存储收敛为唯一 `SecureStore`:Android Keystore 不可导出 AES-256-GCM 密钥,
|
- 敏感存储收敛为唯一 `SecureStore`:Android Keystore 不可导出 AES-256-GCM 密钥,
|
||||||
namespace/key 作为 AAD;删除废弃 security-crypto 依赖。
|
namespace/key 作为 AAD;删除废弃 security-crypto 依赖。
|
||||||
- `sdk-webview/XWebViewView.kt` 的既有未提交修改始终作为用户基线保留;本轮只在该基线
|
- `sdk-webview/XWebViewView.kt` 的既有未提交修改始终作为用户基线保留;本轮只在该基线
|
||||||
上将打开文件能力切换到唯一 `PlatformFileSDK`,未覆盖其它改动。
|
上将打开文件能力切换到唯一 `FileSDK`,未覆盖其它改动。
|
||||||
|
|
||||||
本轮最终已验证:
|
本轮最终已验证:
|
||||||
|
|
||||||
@ -203,16 +203,16 @@ Sample 源码编译时只为静态验证跳过真实签发文件的存在性门
|
|||||||
mapping 上传;mapping 仍保留为本地产物。Debug 只关闭自动采集,可调试宿主在构建未
|
mapping 上传;mapping 仍保留为本地产物。Debug 只关闭自动采集,可调试宿主在构建未
|
||||||
禁用、隐私同意、平台启用时可通过隐藏开发入口显式发送一次测试事件。
|
禁用、隐私同意、平台启用时可通过隐藏开发入口显式发送一次测试事件。
|
||||||
- common 插件按变体生成唯一 Manifest 标记、buildId 与 BugCollect 构建开关;扩展 AAR
|
- common 插件按变体生成唯一 Manifest 标记、buildId 与 BugCollect 构建开关;扩展 AAR
|
||||||
的合并 Provider 未检测到该标记时只退出,不执行初始化,core-only 仍保持零 Manifest。
|
的合并 Provider 未检测到该标记时只退出,不执行初始化,core-only 不触发初始化。
|
||||||
- Update 先服从平台 `features.update`,相同用户重复同步不会再次补检;下载与安装授权
|
- Update 先服从平台 `features.update`,相同用户重复同步不会再次补检;下载与安装授权
|
||||||
绑定当前 generation 和 userId,切换或登出会取消活动下载、清除授权并阻止旧灰度结果安装。
|
绑定当前 generation 和 userId,切换或登出会取消活动下载、清除授权并阻止旧灰度结果安装。
|
||||||
- BugCollect 队列 flush 使用 Mutex 单飞,定时与满批触发不会重复上传同一批数据。
|
- BugCollect 队列 flush 使用 Mutex 单飞,定时与满批触发不会重复上传同一批数据。
|
||||||
- 内部 BugCollect 状态桥接改为私有反射 SPI,不进入 Java/Kotlin 公开 API。
|
- 内部 BugCollect 状态桥接改为私有反射 SPI,不进入 Java/Kotlin 公开 API。
|
||||||
- `sdk-core` 保持零 Manifest,只保留无需初始化的纯本地文件能力。新增唯一 `sdk-file`
|
- `sdk-core` 统一承载本地文件、租户平台上传与 FileProvider 打开;IM、Update、WebView
|
||||||
扩展承载租户平台上传与 FileProvider 打开;IM、Update、WebView 复用该扩展。
|
直接复用 core,不存在第二个文件模块。
|
||||||
- `sdk-file` 的 Uri、File、ByteArray 三个上传入口统一先等待平台初始化,再创建请求体
|
- Uri、File、ByteArray 三个上传入口统一先等待平台初始化,再创建请求体
|
||||||
和读取服务端点;平台失败统一抛出保留 `code/status/message` 且不暴露响应体的
|
和读取服务端点;平台失败统一抛出保留 `code/status/message` 且不暴露响应体的
|
||||||
`PlatformFileException`。`openFile` 仍为不依赖初始化的纯本地能力。
|
`FileUploadException`。`openFile` 仍为不依赖初始化的纯本地能力。
|
||||||
- 全仓日志、标准输出和异常文案不再输出 token、userId、groupId、消息目标、H5
|
- 全仓日志、标准输出和异常文案不再输出 token、userId、groupId、消息目标、H5
|
||||||
payload、请求/响应正文、文件 Uri/绝对路径等实际值。根任务
|
payload、请求/响应正文、文件 Uri/绝对路径等实际值。根任务
|
||||||
`verifyNoSensitiveLogging` 在各模块 `testDebugUnitTest`、`lintRelease` 和 `check`
|
`verifyNoSensitiveLogging` 在各模块 `testDebugUnitTest`、`lintRelease` 和 `check`
|
||||||
|
|||||||
@ -6,10 +6,10 @@
|
|||||||
宿主 App
|
宿主 App
|
||||||
└── 放置租户平台签发的 assets/config/config.xuqmconfig
|
└── 放置租户平台签发的 assets/config/config.xuqmconfig
|
||||||
↓
|
↓
|
||||||
sdk-core(核心层;单独集成时零初始化、零权限、零 Provider)
|
sdk-core(核心层;单独集成时零初始化,文件 Provider 仅用于安全打开/分享)
|
||||||
├── XuqmMergedProvider 实现(仅由扩展 Manifest 注册)
|
├── XuqmMergedProvider 实现(仅由扩展 Manifest 注册)
|
||||||
├── 初始化状态:IDLE / INITIALIZING / READY / DEGRADED / FAILED
|
├── 初始化状态:IDLE / INITIALIZING / READY / DEGRADED / FAILED
|
||||||
├── 七天加密 LKG(仅平台暂不可用时降级使用)
|
├── 严格非敏感 LKG(仅平台暂不可用时降级使用)
|
||||||
├── XuqmSDK.setUserInfo() → 通知所有子模块(反射 / 接口回调)
|
├── XuqmSDK.setUserInfo() → 通知所有子模块(反射 / 接口回调)
|
||||||
├── ApiClient(OkHttp 4.12 + Retrofit 3)
|
├── ApiClient(OkHttp 4.12 + Retrofit 3)
|
||||||
├── TokenStore(Android Keystore + AES-256-GCM)
|
├── TokenStore(Android Keystore + AES-256-GCM)
|
||||||
|
|||||||
@ -4,7 +4,6 @@ kotlin.code.style=official
|
|||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
PUBLISH_VERSION=1.0.5
|
PUBLISH_VERSION=1.0.5
|
||||||
SDK_CORE_VERSION=1.1.6-SNAPSHOT
|
SDK_CORE_VERSION=1.1.6-SNAPSHOT
|
||||||
SDK_FILE_VERSION=1.0.0-SNAPSHOT
|
|
||||||
SDK_IM_VERSION=1.1.3
|
SDK_IM_VERSION=1.1.3
|
||||||
SDK_PUSH_VERSION=1.1.4-SNAPSHOT
|
SDK_PUSH_VERSION=1.1.4-SNAPSHOT
|
||||||
SDK_UPDATE_VERSION=2.0.0-SNAPSHOT
|
SDK_UPDATE_VERSION=2.0.0-SNAPSHOT
|
||||||
|
|||||||
@ -89,7 +89,6 @@ tasks.matching { it.name == "preReleaseBuild" }.configureEach {
|
|||||||
dependencies {
|
dependencies {
|
||||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
implementation(project(":sdk-core"))
|
implementation(project(":sdk-core"))
|
||||||
implementation(project(":sdk-file"))
|
|
||||||
implementation(project(":sdk-bugcollect"))
|
implementation(project(":sdk-bugcollect"))
|
||||||
implementation(project(":sdk-im"))
|
implementation(project(":sdk-im"))
|
||||||
implementation(project(":sdk-push"))
|
implementation(project(":sdk-push"))
|
||||||
|
|||||||
@ -68,7 +68,6 @@ import com.xuqm.sdk.ui.InitialAvatar
|
|||||||
import com.xuqm.sdk.ui.SearchBarField
|
import com.xuqm.sdk.ui.SearchBarField
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import com.xuqm.sdk.file.FileSDK
|
import com.xuqm.sdk.file.FileSDK
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -539,11 +538,7 @@ private fun createCaptureUri(
|
|||||||
): Uri {
|
): Uri {
|
||||||
val dir = context.getExternalFilesDir(directoryType) ?: context.filesDir
|
val dir = context.getExternalFilesDir(directoryType) ?: context.filesDir
|
||||||
val file = File(dir, "$prefix$suffix")
|
val file = File(dir, "$prefix$suffix")
|
||||||
return FileProvider.getUriForFile(
|
return FileSDK.uriForFile(context, file)
|
||||||
context,
|
|
||||||
"${context.packageName}.xuqm.fileprovider",
|
|
||||||
file,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@ -23,9 +23,9 @@ Ed25519 签名,再解密引导配置。V1、未知 keyId、签名错误和包
|
|||||||
`serverUrl` 是签发配置中的唯一平台地址且必填,运行时没有默认地址或跨平台回退。
|
`serverUrl` 是签发配置中的唯一平台地址且必填,运行时没有默认地址或跨平台回退。
|
||||||
配置只包含公开应用身份与平台地址,不包含 `appSecret` 或其它客户端长期认证秘密。
|
配置只包含公开应用身份与平台地址,不包含 `appSecret` 或其它客户端长期认证秘密。
|
||||||
|
|
||||||
只依赖 `sdk-core` 时不会注册初始化 Provider、申请通知权限或合并 FileProvider,
|
只依赖 `sdk-core` 时不会注册初始化 Provider 或申请通知权限,只合并安全打开/分享
|
||||||
基础 HTTP、下载、摘要、MediaStore、时间和安全存储能力无需初始化或登录。引入扩展 SDK 后才由扩展
|
本地文件所需的专用 FileProvider。基础 HTTP、下载、摘要、MediaStore、时间和安全存储
|
||||||
Manifest 合并唯一初始化 Provider。
|
能力无需初始化或登录;引入扩展 SDK 后才由扩展 Manifest 合并唯一初始化 Provider。
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@ -74,9 +74,9 @@ XuqmSDK.tokenStore.clear()
|
|||||||
|
|
||||||
### FileSDK
|
### FileSDK
|
||||||
|
|
||||||
`FileSDK` 只保留无需初始化的本地下载、摘要与 MediaStore 能力。
|
`FileSDK` 提供无需初始化的本地下载、摘要与 MediaStore 能力;
|
||||||
依赖租户平台的上传和依赖 FileProvider 的打开文件能力属于独立 `sdk-file`,
|
同一个 `FileSDK` 还提供上传和 FileProvider 安全打开能力。只有上传会等待平台配置,
|
||||||
统一通过 `PlatformFileSDK` 调用。
|
打开本地文件不要求初始化。
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// 下载
|
// 下载
|
||||||
@ -87,5 +87,5 @@ val file = FileSDK.download(context, url, fileName, destination, notificationTit
|
|||||||
|
|
||||||
`NetworkMonitor` 不要求 SDK 初始化。它只申请 `ACCESS_NETWORK_STATE`,不会为了细分
|
`NetworkMonitor` 不要求 SDK 初始化。它只申请 `ACCESS_NETWORK_STATE`,不会为了细分
|
||||||
蜂窝网络代际申请电话或定位权限;系统不允许读取时稳定返回 `CELLULAR`。
|
蜂窝网络代际申请电话或定位权限;系统不允许读取时稳定返回 `CELLULAR`。
|
||||||
平台配置成功后以加密方式保存七天 LKG。平台暂不可用且 LKG 有效时进入
|
平台配置成功后只保存严格、版本化且不含秘密的最后成功配置。平台暂不可用且缓存结构
|
||||||
`DEGRADED`;没有可用缓存时扩展能力返回结构化错误,宿主核心业务不受影响。
|
有效时进入 `DEGRADED`;没有可用缓存时扩展能力返回结构化错误,宿主核心业务不受影响。
|
||||||
|
|||||||
@ -11,6 +11,11 @@
|
|||||||
|
|
||||||
# ── File SDK ──────────────────────────────────────────────────────────────────
|
# ── File SDK ──────────────────────────────────────────────────────────────────
|
||||||
-keep class com.xuqm.sdk.file.FileSDK { *; }
|
-keep class com.xuqm.sdk.file.FileSDK { *; }
|
||||||
|
-keep class com.xuqm.sdk.file.FileUploadResult { *; }
|
||||||
|
-keep class com.xuqm.sdk.file.FileUploadException { *; }
|
||||||
|
-keepclassmembers class com.xuqm.sdk.file.internal.FileApiResponse {
|
||||||
|
<fields>;
|
||||||
|
}
|
||||||
|
|
||||||
# ── Gson: preserve field names on all SDK model classes ──────────────────────
|
# ── Gson: preserve field names on all SDK model classes ──────────────────────
|
||||||
-keepclassmembers class com.xuqm.sdk.** {
|
-keepclassmembers class com.xuqm.sdk.** {
|
||||||
|
|||||||
@ -5,4 +5,15 @@
|
|||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="28" />
|
android:maxSdkVersion="28" />
|
||||||
|
<application>
|
||||||
|
<provider
|
||||||
|
android:name="com.xuqm.sdk.file.XuqmFileProvider"
|
||||||
|
android:authorities="${applicationId}.xuqm.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/xuqm_file_paths" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@ -17,8 +17,17 @@ import android.webkit.MimeTypeMap
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import com.xuqm.sdk.XuqmSDK
|
||||||
|
import com.xuqm.sdk.core.ServiceEndpointRegistry
|
||||||
|
import com.xuqm.sdk.file.internal.FileApi
|
||||||
|
import com.xuqm.sdk.file.internal.FileUploadGateway
|
||||||
|
import com.xuqm.sdk.file.internal.FileUploadParts
|
||||||
|
import com.xuqm.sdk.file.internal.UploadRequestBodies
|
||||||
|
import com.xuqm.sdk.network.ApiClient
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.MultipartBody
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
@ -39,6 +48,97 @@ data class FileDownloadProgress(
|
|||||||
}
|
}
|
||||||
|
|
||||||
object FileSDK {
|
object FileSDK {
|
||||||
|
private val uploadGateway = FileUploadGateway(
|
||||||
|
awaitInitialization = XuqmSDK::awaitInitialization,
|
||||||
|
apiProvider = {
|
||||||
|
ApiClient.create(FileApi::class.java, ServiceEndpointRegistry.fileBaseUrl)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 从系统 Uri 流式上传;只有上传能力会等待平台统一初始化。 */
|
||||||
|
suspend fun upload(
|
||||||
|
context: Context,
|
||||||
|
uri: Uri,
|
||||||
|
displayName: String? = null,
|
||||||
|
mimeType: String? = null,
|
||||||
|
thumbnailBytes: ByteArray? = null,
|
||||||
|
onProgress: (Int) -> Unit = {},
|
||||||
|
): FileUploadResult = uploadGateway.upload {
|
||||||
|
val name = displayName?.takeIf(String::isNotBlank)
|
||||||
|
?: UploadRequestBodies.resolveDisplayName(context, uri)
|
||||||
|
val type = mimeType?.takeIf(String::isNotBlank)
|
||||||
|
?: context.contentResolver.getType(uri)
|
||||||
|
FileUploadParts(
|
||||||
|
fileName = name,
|
||||||
|
file = MultipartBody.Part.createFormData(
|
||||||
|
"file",
|
||||||
|
name,
|
||||||
|
UploadRequestBodies.createUri(context, uri, type, onProgress),
|
||||||
|
),
|
||||||
|
thumbnailBytes = thumbnailBytes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从文件流式上传,不把文件整体读入内存。 */
|
||||||
|
suspend fun upload(
|
||||||
|
file: File,
|
||||||
|
thumbnailBytes: ByteArray? = null,
|
||||||
|
onProgress: (Int) -> Unit = {},
|
||||||
|
): FileUploadResult = uploadGateway.upload {
|
||||||
|
FileUploadParts(
|
||||||
|
fileName = file.name,
|
||||||
|
file = MultipartBody.Part.createFormData(
|
||||||
|
"file",
|
||||||
|
file.name,
|
||||||
|
UploadRequestBodies.createFile(
|
||||||
|
file,
|
||||||
|
java.net.URLConnection.guessContentTypeFromName(file.name),
|
||||||
|
onProgress,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
thumbnailBytes = thumbnailBytes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun uploadBytes(
|
||||||
|
fileName: String,
|
||||||
|
mimeType: String?,
|
||||||
|
bytes: ByteArray,
|
||||||
|
thumbnailBytes: ByteArray? = null,
|
||||||
|
onProgress: (Int) -> Unit = {},
|
||||||
|
): FileUploadResult = uploadGateway.upload {
|
||||||
|
FileUploadParts(
|
||||||
|
fileName = fileName,
|
||||||
|
file = MultipartBody.Part.createFormData(
|
||||||
|
"file",
|
||||||
|
fileName,
|
||||||
|
UploadRequestBodies.createBytes(mimeType, bytes, onProgress),
|
||||||
|
),
|
||||||
|
thumbnailBytes = thumbnailBytes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 为相机、分享和安装等系统 Intent 生成统一的只读内容 Uri。 */
|
||||||
|
fun uriForFile(context: Context, file: File): Uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.xuqm.fileprovider",
|
||||||
|
file,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 使用 core 唯一 FileProvider 安全打开本地文件;不要求 SDK 初始化。 */
|
||||||
|
fun openFile(context: Context, file: File) {
|
||||||
|
val mimeType = MimeTypeMap.getSingleton()
|
||||||
|
.getMimeTypeFromExtension(file.extension.lowercase())
|
||||||
|
?: "application/octet-stream"
|
||||||
|
val uri = uriForFile(context, file)
|
||||||
|
val view = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, mimeType)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
context.startActivity(Intent.createChooser(view, null).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun downloadToFile(
|
suspend fun downloadToFile(
|
||||||
downloadUrl: String,
|
downloadUrl: String,
|
||||||
@ -350,7 +450,7 @@ object FileSDK {
|
|||||||
/**
|
/**
|
||||||
* 通过 MediaStore 将文件写入公共 Downloads 目录。
|
* 通过 MediaStore 将文件写入公共 Downloads 目录。
|
||||||
* 在华为等设备上直接文件 I/O 会失败,MediaStore 通过 ContentResolver 操作,兼容性更好。
|
* 在华为等设备上直接文件 I/O 会失败,MediaStore 通过 ContentResolver 操作,兼容性更好。
|
||||||
* 返回缓存目录中的副本,便于调用方继续展示、分享或交给可选 sdk-file 打开。
|
* 返回缓存目录中的副本,便于调用方继续展示、分享或通过本对象打开。
|
||||||
*/
|
*/
|
||||||
@androidx.annotation.RequiresApi(Build.VERSION_CODES.Q)
|
@androidx.annotation.RequiresApi(Build.VERSION_CODES.Q)
|
||||||
private fun saveBlobViaMediaStore(context: Context, bytes: ByteArray, fileName: String): File {
|
private fun saveBlobViaMediaStore(context: Context, bytes: ByteArray, fileName: String): File {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package com.xuqm.sdk.file.platform
|
package com.xuqm.sdk.file
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 平台文件服务返回的结构化业务错误。
|
* 平台文件服务返回的结构化业务错误。
|
||||||
@ -6,7 +6,7 @@ package com.xuqm.sdk.file.platform
|
|||||||
* [code]、[status] 与 [message] 均来自服务端响应;HTTP 层无法解析出业务响应时,
|
* [code]、[status] 与 [message] 均来自服务端响应;HTTP 层无法解析出业务响应时,
|
||||||
* code 使用 HTTP 状态码,status 使用 `HTTP_<状态码>`。异常不会携带或输出原始响应体。
|
* code 使用 HTTP 状态码,status 使用 `HTTP_<状态码>`。异常不会携带或输出原始响应体。
|
||||||
*/
|
*/
|
||||||
class PlatformFileException(
|
class FileUploadException(
|
||||||
val code: Int,
|
val code: Int,
|
||||||
val status: String,
|
val status: String,
|
||||||
message: String,
|
message: String,
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.xuqm.sdk.file
|
||||||
|
|
||||||
|
data class FileUploadResult(
|
||||||
|
val url: String,
|
||||||
|
val thumbnailUrl: String? = null,
|
||||||
|
val hash: String,
|
||||||
|
val size: Long,
|
||||||
|
val originalName: String? = null,
|
||||||
|
val mimeType: String? = null,
|
||||||
|
val ext: String? = null,
|
||||||
|
)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.xuqm.sdk.file.platform
|
package com.xuqm.sdk.file
|
||||||
|
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
package com.xuqm.sdk.file.platform
|
package com.xuqm.sdk.file.internal
|
||||||
|
|
||||||
|
import com.xuqm.sdk.file.FileUploadException
|
||||||
|
import com.xuqm.sdk.file.FileUploadResult
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
@ -9,23 +11,23 @@ import retrofit2.http.Multipart
|
|||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.Part
|
import retrofit2.http.Part
|
||||||
|
|
||||||
internal data class PlatformFileApiResponse<T>(
|
internal data class FileApiResponse<T>(
|
||||||
val code: Int? = null,
|
val code: Int? = null,
|
||||||
val status: String? = null,
|
val status: String? = null,
|
||||||
val data: T? = null,
|
val data: T? = null,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal interface PlatformFileApi {
|
internal interface FileApi {
|
||||||
@Multipart
|
@Multipart
|
||||||
@POST("api/file/upload")
|
@POST("api/file/upload")
|
||||||
suspend fun upload(
|
suspend fun upload(
|
||||||
@Part file: MultipartBody.Part,
|
@Part file: MultipartBody.Part,
|
||||||
@Part thumbnail: MultipartBody.Part? = null,
|
@Part thumbnail: MultipartBody.Part? = null,
|
||||||
): PlatformFileApiResponse<FileUploadResult>
|
): FileApiResponse<FileUploadResult>
|
||||||
}
|
}
|
||||||
|
|
||||||
internal data class PlatformFileUploadParts(
|
internal data class FileUploadParts(
|
||||||
val fileName: String,
|
val fileName: String,
|
||||||
val file: MultipartBody.Part,
|
val file: MultipartBody.Part,
|
||||||
val thumbnailBytes: ByteArray?,
|
val thumbnailBytes: ByteArray?,
|
||||||
@ -37,11 +39,11 @@ internal data class PlatformFileUploadParts(
|
|||||||
* 这样 Uri/File/ByteArray 三个公开入口不会在 Provider 异步初始化期间抢先读取端点,
|
* 这样 Uri/File/ByteArray 三个公开入口不会在 Provider 异步初始化期间抢先读取端点,
|
||||||
* 也不会在初始化失败时提前访问 Uri 或文件。
|
* 也不会在初始化失败时提前访问 Uri 或文件。
|
||||||
*/
|
*/
|
||||||
internal class PlatformFileUploadGateway(
|
internal class FileUploadGateway(
|
||||||
private val awaitInitialization: suspend () -> Unit,
|
private val awaitInitialization: suspend () -> Unit,
|
||||||
private val apiProvider: () -> PlatformFileApi,
|
private val apiProvider: () -> FileApi,
|
||||||
) {
|
) {
|
||||||
suspend fun upload(createParts: () -> PlatformFileUploadParts): FileUploadResult {
|
suspend fun upload(createParts: () -> FileUploadParts): FileUploadResult {
|
||||||
awaitInitialization()
|
awaitInitialization()
|
||||||
val parts = createParts()
|
val parts = createParts()
|
||||||
val thumbnail = parts.thumbnailBytes?.takeIf(ByteArray::isNotEmpty)?.let {
|
val thumbnail = parts.thumbnailBytes?.takeIf(ByteArray::isNotEmpty)?.let {
|
||||||
@ -54,24 +56,24 @@ internal class PlatformFileUploadGateway(
|
|||||||
val response = try {
|
val response = try {
|
||||||
apiProvider().upload(parts.file, thumbnail)
|
apiProvider().upload(parts.file, thumbnail)
|
||||||
} catch (error: HttpException) {
|
} catch (error: HttpException) {
|
||||||
throw error.toPlatformFileException()
|
throw error.toFileUploadException()
|
||||||
}
|
}
|
||||||
return response.requireUploadResult()
|
return response.requireUploadResult()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun PlatformFileApiResponse<FileUploadResult>.requireUploadResult(): FileUploadResult {
|
internal fun FileApiResponse<FileUploadResult>.requireUploadResult(): FileUploadResult {
|
||||||
val responseCode = code ?: UNKNOWN_CODE
|
val responseCode = code ?: UNKNOWN_CODE
|
||||||
val responseStatus = status.orEmpty()
|
val responseStatus = status.orEmpty()
|
||||||
val responseMessage = message?.takeIf(String::isNotBlank)
|
val responseMessage = message?.takeIf(String::isNotBlank)
|
||||||
?: "平台文件服务返回无效响应"
|
?: "平台文件服务返回无效响应"
|
||||||
if (responseCode != SUCCESS_CODE || responseStatus != SUCCESS_STATUS) {
|
if (responseCode != SUCCESS_CODE || responseStatus != SUCCESS_STATUS) {
|
||||||
throw PlatformFileException(responseCode, responseStatus, responseMessage)
|
throw FileUploadException(responseCode, responseStatus, responseMessage)
|
||||||
}
|
}
|
||||||
return data ?: throw PlatformFileException(responseCode, responseStatus, responseMessage)
|
return data ?: throw FileUploadException(responseCode, responseStatus, responseMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun HttpException.toPlatformFileException(): PlatformFileException {
|
private fun HttpException.toFileUploadException(): FileUploadException {
|
||||||
val httpCode = code()
|
val httpCode = code()
|
||||||
val parsed = runCatching {
|
val parsed = runCatching {
|
||||||
val body = response()?.errorBody()?.string().orEmpty()
|
val body = response()?.errorBody()?.string().orEmpty()
|
||||||
@ -88,7 +90,7 @@ private fun HttpException.toPlatformFileException(): PlatformFileException {
|
|||||||
?.takeIf(String::isNotBlank)
|
?.takeIf(String::isNotBlank)
|
||||||
Triple(code, status, message)
|
Triple(code, status, message)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
return PlatformFileException(
|
return FileUploadException(
|
||||||
code = parsed?.first ?: httpCode,
|
code = parsed?.first ?: httpCode,
|
||||||
status = parsed?.second ?: "HTTP_$httpCode",
|
status = parsed?.second ?: "HTTP_$httpCode",
|
||||||
message = parsed?.third ?: "平台文件服务请求失败(HTTP $httpCode)",
|
message = parsed?.third ?: "平台文件服务请求失败(HTTP $httpCode)",
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.xuqm.sdk.file.platform.internal
|
package com.xuqm.sdk.file.internal
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -1,5 +1,7 @@
|
|||||||
package com.xuqm.sdk.file.platform
|
package com.xuqm.sdk.file.internal
|
||||||
|
|
||||||
|
import com.xuqm.sdk.file.FileUploadException
|
||||||
|
import com.xuqm.sdk.file.FileUploadResult
|
||||||
import com.xuqm.sdk.XuqmErrorCode
|
import com.xuqm.sdk.XuqmErrorCode
|
||||||
import com.xuqm.sdk.XuqmSdkException
|
import com.xuqm.sdk.XuqmSdkException
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@ -14,12 +16,12 @@ import org.junit.Test
|
|||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
class PlatformFileUploadGatewayTest {
|
class FileUploadGatewayTest {
|
||||||
@Test
|
@Test
|
||||||
fun `successful envelope returns upload data`() {
|
fun `successful envelope returns upload data`() {
|
||||||
val expected = uploadResult()
|
val expected = uploadResult()
|
||||||
|
|
||||||
val actual = PlatformFileApiResponse(
|
val actual = FileApiResponse(
|
||||||
code = 200,
|
code = 200,
|
||||||
status = "0",
|
status = "0",
|
||||||
data = expected,
|
data = expected,
|
||||||
@ -31,8 +33,8 @@ class PlatformFileUploadGatewayTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `successful envelope without data remains structured error`() {
|
fun `successful envelope without data remains structured error`() {
|
||||||
val error = expectPlatformFileException {
|
val error = expectFileUploadException {
|
||||||
PlatformFileApiResponse<FileUploadResult>(
|
FileApiResponse<FileUploadResult>(
|
||||||
code = 200,
|
code = 200,
|
||||||
status = "0",
|
status = "0",
|
||||||
data = null,
|
data = null,
|
||||||
@ -47,8 +49,8 @@ class PlatformFileUploadGatewayTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `business failure preserves server code status and message`() {
|
fun `business failure preserves server code status and message`() {
|
||||||
val error = expectPlatformFileException {
|
val error = expectFileUploadException {
|
||||||
PlatformFileApiResponse<FileUploadResult>(
|
FileApiResponse<FileUploadResult>(
|
||||||
code = 403,
|
code = 403,
|
||||||
status = "1",
|
status = "1",
|
||||||
data = null,
|
data = null,
|
||||||
@ -66,11 +68,11 @@ class PlatformFileUploadGatewayTest {
|
|||||||
val expected = XuqmSdkException(XuqmErrorCode.XUQM_NOT_READY, "SDK 尚未初始化")
|
val expected = XuqmSdkException(XuqmErrorCode.XUQM_NOT_READY, "SDK 尚未初始化")
|
||||||
var partsCreated = false
|
var partsCreated = false
|
||||||
var apiAccessed = false
|
var apiAccessed = false
|
||||||
val gateway = PlatformFileUploadGateway(
|
val gateway = FileUploadGateway(
|
||||||
awaitInitialization = { throw expected },
|
awaitInitialization = { throw expected },
|
||||||
apiProvider = {
|
apiProvider = {
|
||||||
apiAccessed = true
|
apiAccessed = true
|
||||||
fakeApi(PlatformFileApiResponse(200, "0", uploadResult(), "success"))
|
fakeApi(FileApiResponse(200, "0", uploadResult(), "success"))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -96,14 +98,14 @@ class PlatformFileUploadGatewayTest {
|
|||||||
"debugSecret": "do-not-leak"
|
"debugSecret": "do-not-leak"
|
||||||
}
|
}
|
||||||
""".trimIndent().toResponseBody("application/json".toMediaType())
|
""".trimIndent().toResponseBody("application/json".toMediaType())
|
||||||
val gateway = PlatformFileUploadGateway(
|
val gateway = FileUploadGateway(
|
||||||
awaitInitialization = {},
|
awaitInitialization = {},
|
||||||
apiProvider = {
|
apiProvider = {
|
||||||
object : PlatformFileApi {
|
object : FileApi {
|
||||||
override suspend fun upload(
|
override suspend fun upload(
|
||||||
file: MultipartBody.Part,
|
file: MultipartBody.Part,
|
||||||
thumbnail: MultipartBody.Part?,
|
thumbnail: MultipartBody.Part?,
|
||||||
): PlatformFileApiResponse<FileUploadResult> {
|
): FileApiResponse<FileUploadResult> {
|
||||||
throw HttpException(
|
throw HttpException(
|
||||||
Response.error<FileUploadResult>(429, responseBody),
|
Response.error<FileUploadResult>(429, responseBody),
|
||||||
)
|
)
|
||||||
@ -113,8 +115,8 @@ class PlatformFileUploadGatewayTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val error = runCatching { gateway.upload(::uploadParts) }
|
val error = runCatching { gateway.upload(::uploadParts) }
|
||||||
.exceptionOrNull() as? PlatformFileException
|
.exceptionOrNull() as? FileUploadException
|
||||||
?: error("Expected PlatformFileException")
|
?: error("Expected FileUploadException")
|
||||||
|
|
||||||
assertEquals(42901, error.code)
|
assertEquals(42901, error.code)
|
||||||
assertEquals("1", error.status)
|
assertEquals("1", error.status)
|
||||||
@ -122,20 +124,20 @@ class PlatformFileUploadGatewayTest {
|
|||||||
assertFalse(error.message.orEmpty().contains("do-not-leak"))
|
assertFalse(error.message.orEmpty().contains("do-not-leak"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun expectPlatformFileException(block: () -> Unit): PlatformFileException =
|
private fun expectFileUploadException(block: () -> Unit): FileUploadException =
|
||||||
runCatching(block).exceptionOrNull() as? PlatformFileException
|
runCatching(block).exceptionOrNull() as? FileUploadException
|
||||||
?: error("Expected PlatformFileException")
|
?: error("Expected FileUploadException")
|
||||||
|
|
||||||
private fun fakeApi(
|
private fun fakeApi(
|
||||||
response: PlatformFileApiResponse<FileUploadResult>,
|
response: FileApiResponse<FileUploadResult>,
|
||||||
): PlatformFileApi = object : PlatformFileApi {
|
): FileApi = object : FileApi {
|
||||||
override suspend fun upload(
|
override suspend fun upload(
|
||||||
file: MultipartBody.Part,
|
file: MultipartBody.Part,
|
||||||
thumbnail: MultipartBody.Part?,
|
thumbnail: MultipartBody.Part?,
|
||||||
): PlatformFileApiResponse<FileUploadResult> = response
|
): FileApiResponse<FileUploadResult> = response
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun uploadParts() = PlatformFileUploadParts(
|
private fun uploadParts() = FileUploadParts(
|
||||||
fileName = "report.pdf",
|
fileName = "report.pdf",
|
||||||
file = MultipartBody.Part.createFormData(
|
file = MultipartBody.Part.createFormData(
|
||||||
"file",
|
"file",
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.xuqm.sdk.file.platform.internal
|
package com.xuqm.sdk.file.internal
|
||||||
|
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
@ -1,63 +0,0 @@
|
|||||||
# sdk-file
|
|
||||||
|
|
||||||
XuqmGroup Android SDK 的平台文件扩展。它是依赖 `sdk-core` 的可选模块,统一承载:
|
|
||||||
|
|
||||||
- 使用 XuqmSDK 当前登录态和租户平台文件服务上传文件;
|
|
||||||
- 通过模块唯一声明的 `FileProvider` 安全打开本地文件。
|
|
||||||
|
|
||||||
纯下载、SHA-256、MediaStore 保存等无需初始化的能力仍由
|
|
||||||
`com.xuqm.sdk.file.FileSDK` 提供,`sdk-core` 不包含上传转发 API 或 FileProvider。
|
|
||||||
|
|
||||||
## 集成
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
dependencies {
|
|
||||||
implementation("com.xuqm:sdk-file:VERSION")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
宿主同时应用 `com.xuqm.common` 构建插件,并将租户平台签发的
|
|
||||||
`config.xuqmconfig` 放在 `app/src/main/assets/config/config.xuqmconfig`。模块的
|
|
||||||
`XuqmMergedProvider` 只有检测到构建插件生成的标记后才会自动初始化 SDK。
|
|
||||||
|
|
||||||
## 上传
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
val fromUri = PlatformFileSDK.upload(
|
|
||||||
context = context,
|
|
||||||
uri = uri,
|
|
||||||
onProgress = { progress -> /* 0..100 */ },
|
|
||||||
)
|
|
||||||
|
|
||||||
val fromFile = PlatformFileSDK.upload(
|
|
||||||
file = file,
|
|
||||||
onProgress = { progress -> /* 0..100 */ },
|
|
||||||
)
|
|
||||||
|
|
||||||
val fromBytes = PlatformFileSDK.uploadBytes(
|
|
||||||
fileName = "photo.jpg",
|
|
||||||
mimeType = "image/jpeg",
|
|
||||||
bytes = bytes,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
`Uri` 和 `File` 使用流式请求体,不会把完整文件读入内存。返回
|
|
||||||
`FileUploadResult`,字段包括 `url`、`thumbnailUrl`、`hash`、`size`、
|
|
||||||
`originalName`、`mimeType` 和 `ext`。
|
|
||||||
|
|
||||||
三个上传入口都会先等待签名配置和平台服务配置初始化完成,宿主无需自行增加等待
|
|
||||||
逻辑。平台返回失败、状态不一致或成功响应缺少 data 时抛出
|
|
||||||
`PlatformFileException`;其 `code`、`status`、`message` 保留服务端结构化错误,
|
|
||||||
但不会暴露原始响应体。
|
|
||||||
|
|
||||||
## 打开文件
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
PlatformFileSDK.openFile(context, file)
|
|
||||||
```
|
|
||||||
|
|
||||||
打开本地文件不依赖平台初始化或登录。
|
|
||||||
|
|
||||||
模块通过独立的 `XuqmFileProvider` 声明 `${applicationId}.xuqm.fileprovider`。
|
|
||||||
它不会与宿主或其他 SDK 自己的 `FileProvider` 合并冲突,宿主无需添加
|
|
||||||
`tools:replace`。其他模块不得重复声明同一 Xuqm authority。
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.android.library)
|
|
||||||
}
|
|
||||||
|
|
||||||
apply(from = rootProject.file("gradle/publish.gradle"))
|
|
||||||
|
|
||||||
version = providers.gradleProperty("SDK_FILE_VERSION")
|
|
||||||
.orElse(providers.gradleProperty("PUBLISH_VERSION"))
|
|
||||||
.getOrElse("0.1.0-SNAPSHOT")
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.xuqm.sdk.file.platform"
|
|
||||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = libs.versions.minSdk.get().toInt()
|
|
||||||
consumerProguardFiles("consumer-rules.pro")
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
publishing {
|
|
||||||
singleVariant("release") { withSourcesJar() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api(project(":sdk-core"))
|
|
||||||
testImplementation(libs.junit4)
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
-keep class com.xuqm.sdk.file.platform.PlatformFileSDK { *; }
|
|
||||||
-keep class com.xuqm.sdk.file.platform.FileUploadResult { *; }
|
|
||||||
-keep class com.xuqm.sdk.file.platform.PlatformFileException { *; }
|
|
||||||
-keepclassmembers class com.xuqm.sdk.file.platform.PlatformFileApiResponse {
|
|
||||||
<fields>;
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<application>
|
|
||||||
<provider
|
|
||||||
android:name="com.xuqm.sdk.file.platform.XuqmFileProvider"
|
|
||||||
android:authorities="${applicationId}.xuqm.fileprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/xuqm_file_paths" />
|
|
||||||
</provider>
|
|
||||||
<provider
|
|
||||||
android:name="com.xuqm.sdk.internal.XuqmMergedProvider"
|
|
||||||
android:authorities="${applicationId}.xuqm-init"
|
|
||||||
android:exported="false"
|
|
||||||
android:initOrder="50" />
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
package com.xuqm.sdk.file.platform
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import com.xuqm.sdk.XuqmSDK
|
|
||||||
import com.xuqm.sdk.core.ServiceEndpointRegistry
|
|
||||||
import com.xuqm.sdk.file.platform.internal.UploadRequestBodies
|
|
||||||
import com.xuqm.sdk.network.ApiClient
|
|
||||||
import okhttp3.MultipartBody
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
data class FileUploadResult(
|
|
||||||
val url: String,
|
|
||||||
val thumbnailUrl: String? = null,
|
|
||||||
val hash: String,
|
|
||||||
val size: Long,
|
|
||||||
val originalName: String? = null,
|
|
||||||
val mimeType: String? = null,
|
|
||||||
val ext: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 依赖租户平台和 FileProvider 的文件扩展。
|
|
||||||
*
|
|
||||||
* 纯下载、摘要、MediaStore 保存仍属于无需初始化且零 Manifest 的 core FileSDK。
|
|
||||||
*/
|
|
||||||
object PlatformFileSDK {
|
|
||||||
private val uploadGateway = PlatformFileUploadGateway(
|
|
||||||
awaitInitialization = XuqmSDK::awaitInitialization,
|
|
||||||
apiProvider = {
|
|
||||||
ApiClient.create(PlatformFileApi::class.java, ServiceEndpointRegistry.fileBaseUrl)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun upload(
|
|
||||||
context: Context,
|
|
||||||
uri: Uri,
|
|
||||||
displayName: String? = null,
|
|
||||||
mimeType: String? = null,
|
|
||||||
thumbnailBytes: ByteArray? = null,
|
|
||||||
onProgress: (Int) -> Unit = {},
|
|
||||||
): FileUploadResult = uploadGateway.upload {
|
|
||||||
val name = displayName?.takeIf(String::isNotBlank)
|
|
||||||
?: UploadRequestBodies.resolveDisplayName(context, uri)
|
|
||||||
val type = mimeType?.takeIf(String::isNotBlank)
|
|
||||||
?: context.contentResolver.getType(uri)
|
|
||||||
PlatformFileUploadParts(
|
|
||||||
fileName = name,
|
|
||||||
file = MultipartBody.Part.createFormData(
|
|
||||||
"file",
|
|
||||||
name,
|
|
||||||
UploadRequestBodies.createUri(context, uri, type, onProgress),
|
|
||||||
),
|
|
||||||
thumbnailBytes = thumbnailBytes,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun upload(
|
|
||||||
file: File,
|
|
||||||
thumbnailBytes: ByteArray? = null,
|
|
||||||
onProgress: (Int) -> Unit = {},
|
|
||||||
): FileUploadResult = uploadGateway.upload {
|
|
||||||
PlatformFileUploadParts(
|
|
||||||
fileName = file.name,
|
|
||||||
file = MultipartBody.Part.createFormData(
|
|
||||||
"file",
|
|
||||||
file.name,
|
|
||||||
UploadRequestBodies.createFile(
|
|
||||||
file,
|
|
||||||
java.net.URLConnection.guessContentTypeFromName(file.name),
|
|
||||||
onProgress,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
thumbnailBytes = thumbnailBytes,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun uploadBytes(
|
|
||||||
fileName: String,
|
|
||||||
mimeType: String?,
|
|
||||||
bytes: ByteArray,
|
|
||||||
thumbnailBytes: ByteArray? = null,
|
|
||||||
onProgress: (Int) -> Unit = {},
|
|
||||||
): FileUploadResult = uploadGateway.upload {
|
|
||||||
PlatformFileUploadParts(
|
|
||||||
fileName = fileName,
|
|
||||||
file = MultipartBody.Part.createFormData(
|
|
||||||
"file",
|
|
||||||
fileName,
|
|
||||||
UploadRequestBodies.createBytes(mimeType, bytes, onProgress),
|
|
||||||
),
|
|
||||||
thumbnailBytes = thumbnailBytes,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openFile(context: Context, file: File) {
|
|
||||||
val mimeType = MimeTypeMap.getSingleton()
|
|
||||||
.getMimeTypeFromExtension(file.extension.lowercase())
|
|
||||||
?: "application/octet-stream"
|
|
||||||
val uri = FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
"${context.packageName}.xuqm.fileprovider",
|
|
||||||
file,
|
|
||||||
)
|
|
||||||
val view = Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
setDataAndType(uri, mimeType)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
context.startActivity(Intent.createChooser(view, null).apply {
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -31,7 +31,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":sdk-file"))
|
api(project(":sdk-core"))
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
testImplementation(libs.junit4)
|
testImplementation(libs.junit4)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,8 +30,8 @@ import com.xuqm.sdk.im.model.PageResult
|
|||||||
import com.xuqm.sdk.im.model.UserProfile
|
import com.xuqm.sdk.im.model.UserProfile
|
||||||
import com.xuqm.sdk.im.model.BlacklistEntry
|
import com.xuqm.sdk.im.model.BlacklistEntry
|
||||||
import com.xuqm.sdk.im.model.FriendRequest
|
import com.xuqm.sdk.im.model.FriendRequest
|
||||||
import com.xuqm.sdk.file.platform.PlatformFileSDK
|
import com.xuqm.sdk.file.FileSDK
|
||||||
import com.xuqm.sdk.file.platform.FileUploadResult
|
import com.xuqm.sdk.file.FileUploadResult
|
||||||
import com.xuqm.sdk.network.ApiClient
|
import com.xuqm.sdk.network.ApiClient
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -164,7 +164,7 @@ object ImSDK {
|
|||||||
width: Int? = null,
|
width: Int? = null,
|
||||||
height: Int? = null,
|
height: Int? = null,
|
||||||
): ImMessage = withContext(Dispatchers.IO) {
|
): ImMessage = withContext(Dispatchers.IO) {
|
||||||
val result = PlatformFileSDK.upload(file)
|
val result = FileSDK.upload(file)
|
||||||
sendImageMessageWithUploadResult(toId, chatType, result, width, height)
|
sendImageMessageWithUploadResult(toId, chatType, result, width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +200,7 @@ object ImSDK {
|
|||||||
height: Int? = null,
|
height: Int? = null,
|
||||||
durationMs: Long? = null,
|
durationMs: Long? = null,
|
||||||
): ImMessage = withContext(Dispatchers.IO) {
|
): ImMessage = withContext(Dispatchers.IO) {
|
||||||
val result = PlatformFileSDK.upload(file)
|
val result = FileSDK.upload(file)
|
||||||
sendVideoMessageWithUploadResult(toId, chatType, result, width, height, durationMs)
|
sendVideoMessageWithUploadResult(toId, chatType, result, width, height, durationMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +235,7 @@ object ImSDK {
|
|||||||
chatType: String,
|
chatType: String,
|
||||||
file: File,
|
file: File,
|
||||||
): ImMessage = withContext(Dispatchers.IO) {
|
): ImMessage = withContext(Dispatchers.IO) {
|
||||||
val result = PlatformFileSDK.upload(file)
|
val result = FileSDK.upload(file)
|
||||||
sendFileMessageWithUploadResult(toId, chatType, result)
|
sendFileMessageWithUploadResult(toId, chatType, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,7 +264,7 @@ object ImSDK {
|
|||||||
file: File,
|
file: File,
|
||||||
durationMs: Long? = null,
|
durationMs: Long? = null,
|
||||||
): ImMessage = withContext(Dispatchers.IO) {
|
): ImMessage = withContext(Dispatchers.IO) {
|
||||||
val result = PlatformFileSDK.upload(file)
|
val result = FileSDK.upload(file)
|
||||||
sendAudioMessageWithUploadResult(toId, chatType, result, durationMs)
|
sendAudioMessageWithUploadResult(toId, chatType, result, durationMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -49,7 +49,7 @@ data class UpdateCheckResult(
|
|||||||
## 工作原理
|
## 工作原理
|
||||||
|
|
||||||
- `downloadAndInstall` 将 APK 下载到 `getExternalFilesDir(null)`,通过 `FileProvider` 触发系统安装
|
- `downloadAndInstall` 将 APK 下载到 `getExternalFilesDir(null)`,通过 `FileProvider` 触发系统安装
|
||||||
- 安装所需 FileProvider 由依赖的 `sdk-file` 唯一声明,Update 不复制 Manifest 或 paths 资源
|
- 安装所需 FileProvider 由依赖的 `sdk-core` 唯一声明,Update 不复制 Manifest 或 paths 资源
|
||||||
- 安装前依次校验 SHA-256、宿主 packageName 和 APK 签名证书;只允许安装本次检查授权的版本
|
- 安装前依次校验 SHA-256、宿主 packageName 和 APK 签名证书;只允许安装本次检查授权的版本
|
||||||
- 下载任务取消或进程退出后删除 `.part`,下次启动不会自动继续
|
- 下载任务取消或进程退出后删除 `.part`,下次启动不会自动继续
|
||||||
- `PUBLIC_DOWNLOADS` 仅额外导出已校验 APK,不改变 SDK 私有安装源
|
- `PUBLIC_DOWNLOADS` 仅额外导出已校验 APK,不改变 SDK 私有安装源
|
||||||
|
|||||||
@ -31,7 +31,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":sdk-file"))
|
api(project(":sdk-core"))
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
implementation(libs.gson)
|
implementation(libs.gson)
|
||||||
testImplementation(libs.junit4)
|
testImplementation(libs.junit4)
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import com.xuqm.sdk.XuqmSDK
|
import com.xuqm.sdk.XuqmSDK
|
||||||
import com.xuqm.sdk.XuqmInitializationState
|
import com.xuqm.sdk.XuqmInitializationState
|
||||||
import com.xuqm.sdk.XuqmLoginSession
|
import com.xuqm.sdk.XuqmLoginSession
|
||||||
@ -730,11 +729,7 @@ object UpdateSDK {
|
|||||||
val intent = try {
|
val intent = try {
|
||||||
Intent(Intent.ACTION_VIEW).apply {
|
Intent(Intent.ACTION_VIEW).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
val uri = FileProvider.getUriForFile(
|
val uri = FileSDK.uriForFile(context, apkFile)
|
||||||
context,
|
|
||||||
"${context.packageName}.xuqm.fileprovider",
|
|
||||||
apkFile,
|
|
||||||
)
|
|
||||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
}
|
}
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
|
|||||||
@ -36,7 +36,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":sdk-file"))
|
api(project(":sdk-core"))
|
||||||
api(platform(libs.androidx.compose.bom))
|
api(platform(libs.androidx.compose.bom))
|
||||||
api(libs.androidx.ui)
|
api(libs.androidx.ui)
|
||||||
api(libs.androidx.ui.graphics)
|
api(libs.androidx.ui.graphics)
|
||||||
|
|||||||
@ -42,11 +42,9 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import com.xuqm.sdk.file.FileDownloadDestination
|
import com.xuqm.sdk.file.FileDownloadDestination
|
||||||
import com.xuqm.sdk.file.FileSDK
|
import com.xuqm.sdk.file.FileSDK
|
||||||
import com.xuqm.sdk.file.platform.PlatformFileSDK
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -375,7 +373,7 @@ fun XWebViewView(
|
|||||||
}.onSuccess { file ->
|
}.onSuccess { file ->
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
dispatchDownloadEvent("__xwvDownloadDone", url, ",success:true")
|
dispatchDownloadEvent("__xwvDownloadDone", url, ",success:true")
|
||||||
PlatformFileSDK.openFile(context, file)
|
FileSDK.openFile(context, file)
|
||||||
}
|
}
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@ -408,7 +406,7 @@ fun XWebViewView(
|
|||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
dispatchDownloadEvent("__xwvDownloadDone", url, ",success:true")
|
dispatchDownloadEvent("__xwvDownloadDone", url, ",success:true")
|
||||||
// Only open with file viewer when not saved to gallery (e.g. zip, docx)
|
// Only open with file viewer when not saved to gallery (e.g. zip, docx)
|
||||||
if (!savedToGallery) PlatformFileSDK.openFile(context, file)
|
if (!savedToGallery) FileSDK.openFile(context, file)
|
||||||
}
|
}
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
android.util.Log.e("XWV", "blobdownload FAILED", e)
|
android.util.Log.e("XWV", "blobdownload FAILED", e)
|
||||||
@ -533,11 +531,7 @@ fun XWebViewView(
|
|||||||
if (granted && launch) {
|
if (granted && launch) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val imageFile = File.createTempFile("cam_", ".jpg", context.cacheDir)
|
val imageFile = File.createTempFile("cam_", ".jpg", context.cacheDir)
|
||||||
val uri = FileProvider.getUriForFile(
|
val uri = FileSDK.uriForFile(context, imageFile)
|
||||||
context,
|
|
||||||
"${context.packageName}.xuqm.fileprovider",
|
|
||||||
imageFile,
|
|
||||||
)
|
|
||||||
pendingCameraUri.value = uri
|
pendingCameraUri.value = uri
|
||||||
takePictureLauncher.launch(uri)
|
takePictureLauncher.launch(uri)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
@ -580,11 +574,7 @@ fun XWebViewView(
|
|||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val imageFile = File.createTempFile("cam_", ".jpg", context.cacheDir)
|
val imageFile = File.createTempFile("cam_", ".jpg", context.cacheDir)
|
||||||
val uri = FileProvider.getUriForFile(
|
val uri = FileSDK.uriForFile(context, imageFile)
|
||||||
context,
|
|
||||||
"${context.packageName}.xuqm.fileprovider",
|
|
||||||
imageFile,
|
|
||||||
)
|
|
||||||
pendingCameraUri.value = uri
|
pendingCameraUri.value = uri
|
||||||
takePictureLauncher.launch(uri)
|
takePictureLauncher.launch(uri)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
|
|||||||
@ -26,7 +26,6 @@ dependencyResolutionManagement {
|
|||||||
rootProject.name = "XuqmGroupAndroidSDK"
|
rootProject.name = "XuqmGroupAndroidSDK"
|
||||||
|
|
||||||
include(":sdk-core")
|
include(":sdk-core")
|
||||||
include(":sdk-file")
|
|
||||||
include(":sdk-im")
|
include(":sdk-im")
|
||||||
include(":sdk-push")
|
include(":sdk-push")
|
||||||
include(":sdk-update")
|
include(":sdk-update")
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户