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
相关产品推荐
相关产品推荐

