Widget点击打开Content Uri触发SecurityException问题求助
问题:Widget点击打开SAF选择的PDF触发安全异常
问题场景
- 开发一款PDF Widget应用,核心流程:
- Widget配置阶段,通过SAF文件选择器让用户选择PDF文件,获取Content Uri(格式如
content://com.android.providers.downloads.documents/document/msf%3A18) - 将该Uri存入SharedPreferences持久化存储
- 用户点击Widget时,通过PendingIntent启动该Uri,调用第三方PDF阅读器打开文件
- Widget配置阶段,通过SAF文件选择器让用户选择PDF文件,获取Content Uri(格式如
- 实际测试时,点击Widget触发
SecurityException,提示无权限访问目标Content Uri
相关代码
SAF文件选择代码
open class EAHActivity:AppCompatActivity(){ private lateinit var arlPickFile: ActivityResultLauncher<Intent> private var callbackPickFile : (Uri) -> Unit = {} override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val requestType = ActivityResultContracts.StartActivityForResult() arlPickFile = registerForActivityResult(requestType){ val result = it?.resultCode?:RESULT_CANCELED; if(result!= RESULT_OK) return@registerForActivityResult val data = it?.data?.data?:return@registerForActivityResult callbackPickFile.invoke(data) callbackPickFile = {} } } fun requestFile(category: String = Intent.CATEGORY_OPENABLE, filter : String = "application/pdf", onResult:(Uri)->Unit = {}) { callbackPickFile = onResult val baseIntent = Intent(Intent.ACTION_OPEN_DOCUMENT) baseIntent.addCategory(category) baseIntent.type = filter arlPickFile.launch(baseIntent) } }
PendingIntent创建代码
fun getLaunchFilePendingIntent(path:String, ctx: Context):PendingIntent { Log.e("TAG", "path:$path" ) val uri = Uri.parse(path) val openIntent = Intent(Intent.ACTION_VIEW) openIntent.setDataAndType(uri, "application/pdf") openIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION val chooser = Intent.createChooser(openIntent, "Open with") val sdk31PlusFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 return PendingIntent.getActivity(ctx, 123, chooser, PendingIntent.FLAG_UPDATE_CURRENT or sdk31PlusFlag) }
错误信息
java.lang.SecurityException: UID 10146 does not have permission to content://com.android.providers.downloads.documents/document/msf%3A18 [user 0]; you could obtain access using ACTION_OPEN_DOCUMENT or related APIs
解决方案
1. 持久化SAF Uri的访问权限
SAF返回的Uri默认是临时权限,仅在当前应用进程存活时有效。必须在获取Uri后调用takePersistableUriPermission将其转为持久权限,才能在应用重启、Widget触发时继续使用。
修改SAF回调代码:
arlPickFile = registerForActivityResult(requestType){ val result = it?.resultCode?:RESULT_CANCELED; if(result!= RESULT_OK) return@registerForActivityResult val data = it?.data?.data?:return@registerForActivityResult // 添加持久化权限逻辑 val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION contentResolver.takePersistableUriPermission(data, takeFlags) callbackPickFile.invoke(data) callbackPickFile = {} }
2. 修正PendingIntent的权限传递问题
使用Intent.createChooser时,普通的FLAG_GRANT_READ_URI_PERMISSION权限不会自动传递给最终启动的第三方PDF阅读器,需要针对不同版本做适配:
适配Android 12+
使用Intent.FLAG_GRANT_PREFIX_URI_PERMISSION替代普通读权限标记,该标记会将权限传递给选择器启动的目标应用:
fun getLaunchFilePendingIntent(path:String, ctx: Context):PendingIntent { Log.e("TAG", "path:$path" ) val uri = Uri.parse(path) val openIntent = Intent(Intent.ACTION_VIEW) openIntent.setDataAndType(uri, "application/pdf") openIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK // 适配Android 12+的权限传递 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { openIntent.flags = openIntent.flags or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION } else { openIntent.flags = openIntent.flags or Intent.FLAG_GRANT_READ_URI_PERMISSION } val chooser = Intent.createChooser(openIntent, "Open with") val sdk31PlusFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 return PendingIntent.getActivity(ctx, 123, chooser, PendingIntent.FLAG_UPDATE_CURRENT or sdk31PlusFlag) }
兼容低版本(Android 11及以下)
如果需要兼容低版本,可直接去掉createChooser,让系统自动弹出应用选择器,此时FLAG_GRANT_READ_URI_PERMISSION会正常传递:
fun getLaunchFilePendingIntent(path:String, ctx: Context):PendingIntent { Log.e("TAG", "path:$path" ) val uri = Uri.parse(path) val openIntent = Intent(Intent.ACTION_VIEW) openIntent.setDataAndType(uri, "application/pdf") openIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION val sdk31PlusFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 return PendingIntent.getActivity(ctx, 123, openIntent, PendingIntent.FLAG_UPDATE_CURRENT or sdk31PlusFlag) }
核心原理
- 持久化权限:SAF的临时权限会在应用进程销毁后失效,
takePersistableUriPermission将权限转为持久化,允许应用在后续启动、Widget触发时继续访问该Uri。 - 权限传递:
createChooser会创建中间Intent,导致普通权限标记无法传递给目标应用,Android 12+的FLAG_GRANT_PREFIX_URI_PERMISSION专门解决了这个问题;低版本直接使用原始Intent即可让系统处理权限传递。
内容的提问来源于stack exchange,提问作者ansh sachdeva
相关产品推荐
相关产品推荐

