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