如何通过Intent选择目录并将content URI转换为可用的File路径?
我明白你现在卡在哪了——用系统文件选择器选完目录,拿到的不是熟悉的/storage/emulated/0/xxx这种文件路径,而是一串content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata格式的URI,想转成File路径却碰了壁对吧?别慌,我来给你拆解清楚怎么处理这个问题,从权限到存储再到后续操作,一步一步来。
先搞懂为什么拿到的是content URI
从Android 10(API 29)开始,谷歌引入了Scoped Storage(分区存储),为了更安全地管理文件权限,系统不再允许APP直接获取外部存储的文件路径,而是通过**Storage Access Framework(SAF)**返回content URI。这串URI本质是系统给你的“访问凭证”,需要通过系统提供的API来操作对应的目录。
第一步:获取持久化的URI权限
你选完目录拿到URI后,一定要先获取持久化权限,不然重启APP后这个URI就失效了。代码示例如下:
// 假设你用ActivityResultContracts来启动目录选择(推荐新API) val pickDirLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> uri?.let { // 申请读写权限的持久化授权 val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION contentResolver.takePersistableUriPermission(it, flags) // 接下来就可以处理存储和文件操作了 handleDirectoryUri(it) } } // 启动目录选择器 pickDirLauncher.launch(null)
第二步:把URI存储到SharedPreferences
直接把URI的字符串形式存进去就行,后续取出时再转成Uri对象:
private fun handleDirectoryUri(uri: Uri) { // 存储到SharedPreferences val prefs = getSharedPreferences("AppStoragePrefs", MODE_PRIVATE) prefs.edit() .putString("selected_directory_uri", uri.toString()) .apply() // 这里可以直接开始文件操作 performFileOperations(uri) } // 后续取出URI的方法 fun getSavedDirectoryUri(): Uri? { val prefs = getSharedPreferences("AppStoragePrefs", MODE_PRIVATE) val uriString = prefs.getString("selected_directory_uri", null) return uriString?.let { Uri.parse(it) } }
第三步:用DocumentFile实现文件操作(推荐方式)
别执着于转成File对象了,用DocumentFile可以直接基于content URI完成所有你需要的操作,而且完全适配Scoped Storage:
private fun performFileOperations(uri: Uri) { val targetDir = DocumentFile.fromTreeUri(this, uri) ?: return // 1. 获取父目录 val parentDir = targetDir.parentFile parentDir?.let { Log.d("DirOp", "父目录名称:${it.name}") } // 2. 列出目录下所有文件/子目录 val files = targetDir.listFiles() files?.forEach { file -> val type = if (file.isDirectory) "目录" else "文件" Log.d("DirOp", "${file.name} - $type") } // 3. 还能创建文件/目录 val newFile = targetDir.createFile("text/plain", "new_note.txt") val newSubDir = targetDir.createDirectory("new_sub_folder") }
可选:将Primary存储的URI转成File路径
如果你的场景只需要处理主外部存储(也就是primary开头的URI),可以解析出对应的File路径,但注意这只适用于主存储,SD卡或其他存储的URI没法这么转:
fun getFilePathFromPrimaryTreeUri(uri: Uri): String? { val docId = DocumentsContract.getTreeDocumentId(uri) val split = docId.split(":") // 仅处理primary存储的情况 if (split.firstOrNull() == "primary") { return "${Environment.getExternalStorageDirectory().absolutePath}/${split.last()}" } return null }
比如你拿到的primary%3AAndroid%2Fdata解码后是primary:Android/data,转成File路径就是/storage/emulated/0/Android/data。
最后提醒
尽量优先用DocumentFile或ContentResolver来操作文件,直接转File路径的方式在Android 11+会越来越受限,容易触发权限问题。
内容的提问来源于stack exchange,提问作者tomas

