
本文详解如何通过 takePersistableUriPermission() 持久化获取用户通过 OpenDocument 选择的本地 MP4 文件 URI 权限,从而在 App 重启后无需重复选择即可直接播放,彻底解决 SecurityException 和临时权限失效问题。
本文详解如何通过 takepersistableuripermission() 持久化获取用户通过 opendocument 选择的本地 mp4 文件 uri 权限,从而在 app 重启后无需重复选择即可直接播放,彻底解决 securityexception 和临时权限失效问题。
在 Android 开发中,使用 ActivityResultLauncher(如 ActivityResultContracts.OpenDocument)选取本地视频文件(如 MP4)后,系统仅授予临时读取权限——该权限会在 Activity 销毁、进程重启或设备休眠后自动失效。因此,当用户关闭 App 后再次启动,直接用之前保存的 Uri 调用 ExoPlayer 播放时,会抛出类似以下异常:
java.lang.SecurityException: UID 10790 does not have permission to content://com.android.providers.media.documents/document/video%3A1000000431 [user 0]; you could obtain access using ACTION_OPEN_DOCUMENT or related APIs
这是因为 grantUriPermission() 仅对目标包生效且不具备持久性,而正确解法是调用 ContentResolver.takePersistableUriPermission() —— 它可向系统申请长期有效的读/写权限(需用户授权一次),并支持跨进程、跨生命周期复用。
✅ 正确实现步骤
-
在选取文件回调中立即申请持久化权限
必须在ActivityResultCallback.onActivityResult()内、获取到Uri后立刻调用takePersistableUriPermission(),且仅支持Intent.FLAG_GRANT_READ_URI_PERMISSION(视频播放只需读权限):
private val videoFilePickerLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri: Uri? ->
uri?.let { selectedUri ->
videoUri = selectedUri
// ? 关键:申请持久化读权限(必须在回调内执行)
contentResolver.takePersistableUriPermission(
selectedUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
// 可选:持久化保存 Uri(如 SharedPreferences)
saveVideoUriToPrefs(selectedUri)
}
}
-
安全地持久化存储 URI
使用Uri.toString()保存字符串(不可序列化Uri对象本身),并在恢复时用Uri.parse()解析:
// 保存
private fun saveVideoUriToPrefs(uri: Uri) {
getSharedPreferences("app_prefs", Context.MODE_PRIVATE).edit()
.putString("last_video_uri", uri.toString())
.apply()
}
// 恢复(App 启动后)
private fun loadVideoUriFromPrefs(): Uri? {
return getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
.getString("last_video_uri", null)
?.let { Uri.parse(it) }
}
-
播放前验证权限有效性(推荐)
即使已申请持久权限,某些场景(如用户手动重置应用权限、系统清理)可能导致权限失效。建议播放前校验:
private fun canReadUri(uri: Uri): Boolean {
return try {
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
cursor.count > 0
} ?: false
} catch (e: SecurityException) {
Log.w("VideoPlayer", "No read permission for URI: $uri", e)
false
}
}
// 使用示例
val savedUri = loadVideoUriFromPrefs()
if (savedUri != null && canReadUri(savedUri)) {
val mediaItem = MediaItem.fromUri(savedUri)
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.play()
} else {
// 权限失效,引导用户重新选择
showToast("视频权限已过期,请重新选择文件")
videoFilePickerLauncher.launch(arrayOf("video/*"))
}
⚠️ 注意事项
-
仅适用于
content://URI:takePersistableUriPermission()对file://不生效,务必使用OpenDocument(而非GetContent)获取文档提供者 URI。 -
权限范围严格匹配:传入的
Intent.FLAG_*_URI_PERMISSION必须与OpenDocument返回的 URI 类型一致(如视频需video/*)。 -
Android 11+ 限制:若目标 SDK ≥ 30(Android 11),
content://URI 的持久权限仍受 Scoped Storage 约束,但OpenDocument+takePersistableUriPermission是官方推荐的合规方案。 -
避免内存泄漏:
videoUri应作为Activity/Fragment成员变量管理,避免在ViewModel中持有未清理的Uri引用。
✅ 总结
要实现“一次选择、永久播放”的本地视频体验,核心在于:
✅ 使用 ActivityResultContracts.OpenDocument 获取 content:// URI;
✅ 在 onActivityResult 中立即调用 contentResolver.takePersistableUriPermission();
✅ 安全持久化 URI 字符串,并在播放前做权限校验;
❌ 禁用 grantUriPermission()(无持久性)和 file:// 路径(不兼容 Scoped Storage)。
此方案完全符合 Android 权限最佳实践,已在 Android 8.0–14 上稳定运行,是构建用户友好型媒体播放器的可靠基础。











