feat(sdk): 扩展用户信息功能并增强文件下载权限管理
- 扩展 setUserInfo 方法支持用户昵称和头像参数 - 重构 XuqmUserInfo 数据类使用 id 替代 userId 字段 - 更新 UpdateSDK 中用户ID解析逻辑适配新字段名 - 为 Android 11+ 设备添加 MANAGE_EXTERNAL_STORAGE 权限检查 - 实现下载暂停和恢复机制处理存储权限请求流程 - 添加权限说明对话框指导用户完成设置授权 - 集成生命周期观察者自动续传获得权限后的下载任务
这个提交包含在:
父节点
db8a2a6820
当前提交
16c6668533
@ -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 = {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户