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

Android R下替代getExternalStoragePublicDirectory实现缓存文件转存Downloads目录

Android R 静默写入公共Downloads目录正确实现方案

问题根因

你遇到的EEXIST报错但Downloads目录下无对应文件的问题,核心是Android 10开始启用分区存储机制,公共目录的文件元数据由MediaStore数据库统一管理,就算物理文件被删除,数据库中如果残留同名文件的记录,插入同名文件时就会触发该报错,和本地物理文件是否真实存在无关。你遇到的0字节问题一般是输出流未正确flush、关闭导致的。

前置权限说明

  • Android 10(API 29)及以上:无需申请WRITE_EXTERNAL_STORAGE权限,直接通过MediaStore即可写入公共Downloads目录,全程无UI弹窗,完全静默
  • 如需兼容Android 9及更低版本,再单独申请存储权限即可

核心实现代码

1. 构造文件元数据参数

val targetFileName = "test.zip" // 替换为你的压缩包实际文件名
val contentValues = ContentValues().apply {
    put(MediaStore.Downloads.DISPLAY_NAME, targetFileName)
    put(MediaStore.Downloads.MIME_TYPE, "application/zip")
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // 指定存储到公共Downloads根目录,需要子目录可以自行追加路径,比如"Downloads/我的备份"
        put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
        // 标记文件处于写入中状态,避免其他应用提前读取到不完整文件
        put(MediaStore.Downloads.IS_PENDING, 1)
    }
}

2. 处理MediaStore重复记录,获取目标文件Uri

val contentResolver = context.contentResolver
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
    MediaStore.Files.getContentUri("external")
}

// 先查询MediaStore是否存在同名文件的记录,避免EEXIST报错
var existId: Long? = null
contentResolver.query(
    collection,
    arrayOf(MediaStore.Downloads._ID),
    "${MediaStore.Downloads.DISPLAY_NAME} = ?",
    arrayOf(targetFileName),
    null
)?.use { cursor ->
    if (cursor.moveToFirst()) {
        existId = cursor.getLong(0)
    }
}

val targetUri = if (existId != null) {
    // 存在旧记录,可直接复用,也可调用contentResolver.delete删除旧记录后重新插入
    ContentUris.withAppendedId(collection, existId!!)
} else {
    contentResolver.insert(collection, contentValues) ?: throw RuntimeException("创建目标文件失败")
}

3. 写入缓存文件内容到目标路径

// 替换为你缓存目录下的源压缩包文件
val sourceFile = File(context.cacheDir, "source.zip")

// 复制文件内容,use关键字会自动关闭流,避免资源泄漏
contentResolver.openOutputStream(targetUri)?.use { outputStream ->
    FileInputStream(sourceFile).use { inputStream ->
        inputStream.copyTo(outputStream)
        outputStream.flush()
    }
} ?: throw RuntimeException("打开输出流失败")

// 写入完成后更新标记,通知系统文件可访问
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    contentValues.clear()
    contentValues.put(MediaStore.Downloads.IS_PENDING, 0)
    contentResolver.update(targetUri, contentValues, null, null)
}

常见问题排查

  • 若出现0字节文件,优先检查输出流是否正确flush、关闭,不要手动管理流的生命周期,优先使用use关键字自动处理
  • 若仍出现EEXIST报错,可先调用contentResolver.delete删除旧的MediaStore记录后再重新插入
  • 不需要申请任何特殊权限,也无需调用ACTION_OPEN_DOCUMENT_TREE、ACTION_CREATE_DOCUMENT这类会弹UI的API

内容的提问来源于stack exchange,提问作者user16298059

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 16:48:03