Android使用SAF递归复制文件夹时目录层级丢失问题排查
问题根因
核心逻辑错误源于对DocumentFile.fromTreeUri()的API行为理解错误:
- 该方法仅会解析传入Uri中
tree/<treeId>字段对应的目录作为操作根,会自动忽略Uri后续/document/<docId>字段指向的子路径。 - 原代码递归时虽然逐层传递了新建子目录的Uri,但每一层重新调用
DocumentFile.fromTreeUri(context, targetDirectory)时,都会直接跳回用户最初选择的顶层树目录执行创建操作,最终导致所有文件、文件夹全部平铺在根目录。 - 附带问题:创建文件时MIME类型硬编码为
application/image,本身属于无效MIME值,非图片类文件会出现类型标记异常。
修复方案
调整递归函数的参数定义,将目标目录参数从Uri类型改为DocumentFile类型,递归全程传递已定位到对应层级的DocumentFile实例,避免重复解析TreeUri导致的路径回退问题。
修复后的递归实现
import android.webkit.MimeTypeMap private fun copyFolder(source: File, targetParentDir: DocumentFile) { when { source.isDirectory -> { // 在当前父目录下创建源目录对应的同名子目录 val currentTargetDir = targetParentDir.createDirectory(source.name) ?: return // 遍历源目录下所有条目,传入新创建的子目录作为下一级的父目录 source.listFiles()?.forEach { entry -> copyFolder(entry, currentTargetDir) } } source.isFile -> { // 根据文件扩展名自动获取MIME类型,兜底使用通用二进制流类型 val mimeType = MimeTypeMap.getSingleton() .getMimeTypeFromExtension(source.extension.lowercase()) ?: "application/octet-stream" // 在当前父目录下创建目标文件 val targetFile = targetParentDir.createFile(mimeType, source.name) ?: return // 执行文件流复制 context.contentResolver.openOutputStream(targetFile.uri)?.buffered()?.use { output -> source.inputStream().buffered().use { input -> input.copyTo(output) } } } } }
调整调用入口
首次拿到用户选择的树Uri后,先转换为根目录对应的DocumentFile实例,再传入递归函数:
private val exportLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri? -> uri ?: return@registerForActivityResult val rootTargetDir = DocumentFile.fromTreeUri(context, uri) ?: return@registerForActivityResult copyFolder(context.getExternalFilesDir(Environment.DIRECTORY_MOVIES), rootTargetDir) } exportLauncher.launch(null)
实现说明
- 递归过程不再重复调用
fromTreeUri解析Uri,每一层的创建操作都直接在传入的父级DocumentFile节点下执行,路径层级和源目录完全一一对应,可完整保留原有目录结构。 - MIME类型自动匹配逻辑覆盖了所有常见文件类型,避免硬编码导致的文件类型识别错误。
- 增加了空值兜底逻辑,避免权限不足、创建失败时的空指针异常。
内容的提问来源于stack exchange,提问作者GenError
相关产品推荐
相关产品推荐

