You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android使用SAF递归复制文件夹时目录层级丢失问题排查

问题根因

核心逻辑错误源于对DocumentFile.fromTreeUri()的API行为理解错误:

  1. 该方法仅会解析传入Uri中tree/<treeId>字段对应的目录作为操作根,会自动忽略Uri后续/document/<docId>字段指向的子路径。
  2. 原代码递归时虽然逐层传递了新建子目录的Uri,但每一层重新调用DocumentFile.fromTreeUri(context, targetDirectory)时,都会直接跳回用户最初选择的顶层树目录执行创建操作,最终导致所有文件、文件夹全部平铺在根目录。
  3. 附带问题:创建文件时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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 14:45:28