fix(file): 公共 Downloads 使用 MediaStore 写入,兼容华为设备

- Android 10+ 公共目录通过 MediaStore ContentResolver 写入
- 同时写入缓存文件供 openFile 使用
- 避免华为设备 mkdirs()/writeBytes() ENOENT 问题
这个提交包含在:
XuqmGroup 2026-06-18 13:56:35 +08:00
父节点 d2873eda66
当前提交 30ce89afcc

查看文件

@ -251,7 +251,12 @@ object FileSDK {
val safeName = fileName.takeIf { it.isNotBlank() } ?: "download.bin" val safeName = fileName.takeIf { it.isNotBlank() } ?: "download.bin"
// 尝试目标目录,如果 PublicDownloads 失败则回退到 Sandbox // Android 10+ 公共 Downloads 目录使用 MediaStore 写入(兼容华为等设备)
if (destination == FileDownloadDestination.PublicDownloads && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return saveBlobViaMediaStore(context, bytes, safeName)
}
// 旧版本或 Sandbox 目录直接写文件
val baseDir = resolveBaseDir(context, destination) val baseDir = resolveBaseDir(context, destination)
val target = uniqueFile(baseDir, safeName) val target = uniqueFile(baseDir, safeName)
android.util.Log.d("XWV", "saveBlobDownload: writing to ${target.absolutePath}") android.util.Log.d("XWV", "saveBlobDownload: writing to ${target.absolutePath}")
@ -269,6 +274,44 @@ object FileSDK {
} }
} }
/**
* 通过 MediaStore 将文件写入公共 Downloads 目录
* 在华为等设备上直接文件 I/O 会失败MediaStore 通过 ContentResolver 操作兼容性更好
* 返回缓存目录中的副本真实文件可被 openFile 使用
*/
private fun saveBlobViaMediaStore(context: Context, bytes: ByteArray, fileName: String): File {
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(fileName.substringAfterLast('.', "").lowercase())
?: "application/octet-stream"
val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
// 写入 MediaStore
val values = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
put(MediaStore.Downloads.MIME_TYPE, mimeType)
put(MediaStore.Downloads.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/$appName")
put(MediaStore.Downloads.IS_PENDING, 1)
}
val resolver = context.contentResolver
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
?: throw java.io.IOException("MediaStore insert returned null")
resolver.openOutputStream(uri)?.use { out ->
out.write(bytes)
} ?: throw java.io.IOException("Cannot open output stream for $uri")
values.clear()
values.put(MediaStore.Downloads.IS_PENDING, 0)
resolver.update(uri, values, null, null)
android.util.Log.d("XWV", "saveBlobViaMediaStore: saved $fileName, size=${bytes.size}, uri=$uri")
// 同时写入缓存目录,供 openFile 使用
val cacheFile = File(context.cacheDir, fileName).apply { writeBytes(bytes) }
return cacheFile
}
private fun resolveBaseDir(context: Context, destination: FileDownloadDestination): File = when (destination) { private fun resolveBaseDir(context: Context, destination: FileDownloadDestination): File = when (destination) {
FileDownloadDestination.PublicDownloads -> { FileDownloadDestination.PublicDownloads -> {
val appName = context.applicationInfo.loadLabel(context.packageManager).toString() val appName = context.applicationInfo.loadLabel(context.packageManager).toString()