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 等服务使用灰度发布精准推送等 * 设置用户信息 update / push / license 等服务使用灰度发布精准推送等
* IM 服务需要独立调用 [login] 完成鉴权[setUserInfo] IM socket 连接无效 * IM 服务需要独立调用 [login] 完成鉴权[setUserInfo] IM socket 连接无效
* *
* @param userId 用户标识 null 则清除 * @param id 用户唯一标识 null 或空字符串则清除用户信息
* @param name 用户显示名称可选
* @param avatar 用户头像 URL可选
*/ */
fun setUserInfo(userId: String?) { fun setUserInfo(id: String?, name: String? = null, avatar: String? = null) {
userInfoValue = userId?.takeIf { it.isNotBlank() }?.let { XuqmUserInfo(it) } userInfoValue = id?.takeIf { it.isNotBlank() }?.let {
XuqmUserInfo(id = it, name = name, avatar = avatar)
}
} }
suspend fun login( suspend fun login(

查看文件

@ -1,5 +1,10 @@
package com.xuqm.sdk package com.xuqm.sdk
data class XuqmUserInfo( 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 * 优先级[XuqmSDK.userInfo]?.userId > [XuqmSDK.currentLoginSession]?.userId
*/ */
private fun resolveUserId(): String? { private fun resolveUserId(): String? {
return XuqmSDK.userInfo?.userId ?: XuqmSDK.currentLoginSession?.userId return XuqmSDK.userInfo?.id ?: XuqmSDK.currentLoginSession?.userId
} }
private fun normalizeDownloadUrl(rawUrl: String?): String? { private fun normalizeDownloadUrl(rawUrl: String?): String? {

查看文件

@ -33,10 +33,13 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text 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.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.xuqm.sdk.file.FileSDK import com.xuqm.sdk.file.FileSDK
@ -225,6 +228,8 @@ fun XWebViewView(
var currentUrl by remember { mutableStateOf<String?>(config.url.ifBlank { null }) } var currentUrl by remember { mutableStateOf<String?>(config.url.ifBlank { null }) }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var showImageSourceDialog by remember { mutableStateOf(false) } var showImageSourceDialog by remember { mutableStateOf(false) }
var showStoragePermissionDialog by remember { mutableStateOf(false) }
val pendingUrlDownload = remember { mutableStateOf<Pair<String, String?>?>(null) }
val notificationPermissionLauncher = rememberLauncherForActivityResult( val notificationPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission() ActivityResultContracts.RequestPermission()
@ -249,6 +254,17 @@ fun XWebViewView(
"download" -> { "download" -> {
val url = payload.optString("url").takeIf { it.isNotBlank() } ?: return@handler val url = payload.optString("url").takeIf { it.isNotBlank() } ?: return@handler
val filename = payload.optString("filename").takeIf { it.isNotBlank() } 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 && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
config.downloadNotificationTitle != null && config.downloadNotificationTitle != null &&
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED 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 = { val launchCamera: () -> Unit = {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
runCatching { 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) { if (showImageSourceDialog) {
AlertDialog( AlertDialog(
onDismissRequest = { onDismissRequest = {