From 16c6668533440ba475cf40673f427b2c5f89adc2 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Thu, 11 Jun 2026 16:41:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(sdk):=20=E6=89=A9=E5=B1=95=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BF=A1=E6=81=AF=E5=8A=9F=E8=83=BD=E5=B9=B6=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E6=96=87=E4=BB=B6=E4=B8=8B=E8=BD=BD=E6=9D=83=E9=99=90?= =?UTF-8?q?=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 扩展 setUserInfo 方法支持用户昵称和头像参数 - 重构 XuqmUserInfo 数据类使用 id 替代 userId 字段 - 更新 UpdateSDK 中用户ID解析逻辑适配新字段名 - 为 Android 11+ 设备添加 MANAGE_EXTERNAL_STORAGE 权限检查 - 实现下载暂停和恢复机制处理存储权限请求流程 - 添加权限说明对话框指导用户完成设置授权 - 集成生命周期观察者自动续传获得权限后的下载任务 --- .../src/main/java/com/xuqm/sdk/XuqmSDK.kt | 10 ++- .../main/java/com/xuqm/sdk/XuqmUserInfo.kt | 7 +- .../java/com/xuqm/sdk/update/UpdateSDK.kt | 2 +- .../java/com/xuqm/sdk/webview/XWebViewView.kt | 85 +++++++++++++++++++ 4 files changed, 99 insertions(+), 5 deletions(-) diff --git a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt index f111d1f..c44bd05 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt @@ -167,10 +167,14 @@ object XuqmSDK { * 设置用户信息,供 update / push / license 等服务使用(灰度发布、精准推送等)。 * IM 服务需要独立调用 [login] 完成鉴权,[setUserInfo] 对 IM socket 连接无效。 * - * @param userId 用户标识,传 null 则清除 + * @param id 用户唯一标识,传 null 或空字符串则清除用户信息 + * @param name 用户显示名称(可选) + * @param avatar 用户头像 URL(可选) */ - fun setUserInfo(userId: String?) { - userInfoValue = userId?.takeIf { it.isNotBlank() }?.let { XuqmUserInfo(it) } + fun setUserInfo(id: String?, name: String? = null, avatar: String? = null) { + userInfoValue = id?.takeIf { it.isNotBlank() }?.let { + XuqmUserInfo(id = it, name = name, avatar = avatar) + } } suspend fun login( diff --git a/sdk-core/src/main/java/com/xuqm/sdk/XuqmUserInfo.kt b/sdk-core/src/main/java/com/xuqm/sdk/XuqmUserInfo.kt index 3d6bf20..619cad7 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/XuqmUserInfo.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/XuqmUserInfo.kt @@ -1,5 +1,10 @@ package com.xuqm.sdk data class XuqmUserInfo( - val userId: String, + /** 用户唯一标识(必填),用于灰度发布、精准推送等 */ + val id: String, + /** 用户显示名称(可选) */ + val name: String? = null, + /** 用户头像 URL(可选) */ + val avatar: String? = null, ) 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 ff20f60..7074666 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 @@ -25,7 +25,7 @@ object UpdateSDK { * 优先级:[XuqmSDK.userInfo]?.userId > [XuqmSDK.currentLoginSession]?.userId */ private fun resolveUserId(): String? { - return XuqmSDK.userInfo?.userId ?: XuqmSDK.currentLoginSession?.userId + return XuqmSDK.userInfo?.id ?: XuqmSDK.currentLoginSession?.userId } private fun normalizeDownloadUrl(rawUrl: String?): String? { 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 44586a9..b15a887 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 @@ -33,10 +33,13 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import com.xuqm.sdk.file.FileSDK @@ -225,6 +228,8 @@ fun XWebViewView( var currentUrl by remember { mutableStateOf(config.url.ifBlank { null }) } val coroutineScope = rememberCoroutineScope() var showImageSourceDialog by remember { mutableStateOf(false) } + var showStoragePermissionDialog by remember { mutableStateOf(false) } + val pendingUrlDownload = remember { mutableStateOf?>(null) } val notificationPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() @@ -249,6 +254,17 @@ fun XWebViewView( "download" -> { val url = payload.optString("url").takeIf { it.isNotBlank() } ?: return@handler val filename = payload.optString("filename").takeIf { it.isNotBlank() } + // On API 30+, writing to public Downloads requires MANAGE_EXTERNAL_STORAGE. + // Show a rationale dialog; after the user grants permission and returns, the + // lifecycle observer will resume the download automatically. + if (config.downloadDestination == FileDownloadDestination.PublicDownloads && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + !FileSDK.isManageStorageGranted(context) + ) { + pendingUrlDownload.value = url to filename + showStoragePermissionDialog = true + return@handler + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && config.downloadNotificationTitle != null && ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED @@ -379,6 +395,52 @@ fun XWebViewView( } } + // When returning from the MANAGE_EXTERNAL_STORAGE settings page, resume pending download. + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + val pd = pendingUrlDownload.value + if (pd != null && FileSDK.isManageStorageGranted(context)) { + val (url, filename) = pd + pendingUrlDownload.value = null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + config.downloadNotificationTitle != null && + ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED + ) { + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + coroutineScope.launch(Dispatchers.IO) { + runCatching { + FileSDK.download( + context = context, + downloadUrl = url, + fileName = filename, + destination = config.downloadDestination, + notificationTitle = config.downloadNotificationTitle, + ) { progress -> + mainHandler.post { + dispatchDownloadEvent("__xwvDownloadProgress", url, ",progress:$progress") + } + } + }.onSuccess { file -> + withContext(Dispatchers.Main) { + dispatchDownloadEvent("__xwvDownloadDone", url, ",success:true") + FileSDK.openFile(context, file) + } + }.onFailure { e -> + withContext(Dispatchers.Main) { + dispatchDownloadEvent("__xwvDownloadDone", url, ",success:false,error:'${e.message?.escapeJs()}'") + } + } + } + } + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } + val launchCamera: () -> Unit = { if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { runCatching { @@ -496,6 +558,29 @@ fun XWebViewView( }, ) + if (showStoragePermissionDialog) { + AlertDialog( + onDismissRequest = { + showStoragePermissionDialog = false + pendingUrlDownload.value = null + }, + title = { Text("需要存储权限") }, + text = { Text("下载文件需要"所有文件访问权限",请在设置中授权后将自动继续下载。") }, + confirmButton = { + TextButton(onClick = { + showStoragePermissionDialog = false + FileSDK.requestManageStorageIntent(context)?.let { context.startActivity(it) } + }) { Text("前往设置") } + }, + dismissButton = { + TextButton(onClick = { + showStoragePermissionDialog = false + pendingUrlDownload.value = null + }) { Text("取消") } + }, + ) + } + if (showImageSourceDialog) { AlertDialog( onDismissRequest = {