feat(sdk): 扩展用户信息功能并增强文件下载权限管理

- 扩展 setUserInfo 方法支持用户昵称和头像参数
- 重构 XuqmUserInfo 数据类使用 id 替代 userId 字段
- 更新 UpdateSDK 中用户ID解析逻辑适配新字段名
- 为 Android 11+ 设备添加 MANAGE_EXTERNAL_STORAGE 权限检查
- 实现下载暂停和恢复机制处理存储权限请求流程
- 添加权限说明对话框指导用户完成设置授权
- 集成生命周期观察者自动续传获得权限后的下载任务
这个提交包含在:
XuqmGroup 2026-06-11 16:41:07 +08:00
父节点 db8a2a6820
当前提交 16c6668533
共有 4 个文件被更改,包括 99 次插入5 次删除

查看文件

@ -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(

查看文件

@ -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,
)

查看文件

@ -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? {

查看文件

@ -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<String?>(config.url.ifBlank { null }) }
val coroutineScope = rememberCoroutineScope()
var showImageSourceDialog by remember { mutableStateOf(false) }
var showStoragePermissionDialog by remember { mutableStateOf(false) }
val pendingUrlDownload = remember { mutableStateOf<Pair<String, String?>?>(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 = {