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

Android中如何从已保存的Uri目录创建图片/GIF文件?

问题描述

我的APP允许用户下载其他用户的图片/GIF,支持选择保存位置。目前通过Intent.ACTION_OPEN_DOCUMENT_TREE让用户选择目录,将Uri保存到DataStore,后续从DataStore读取Uri。现在需要把ByteArray格式的图片/GIF(支持png、webp、jpg、jpeg、gif)保存到该目录,但调用DocumentFile.createFile时返回null,无法创建文件,查找文件也失败,无法完成写入。


现有代码片段

选择保存目录的Intent

val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
    Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}

保存Uri到DataStore

suspend fun setSaveLocationUri(p0: Context, saveUri: Uri) {
    val savePath = stringPreferencesKey("save_path_uri")
    p0.dataStore.edit { settings ->
        settings[savePath] = saveUri.toString()
    }
}

从DataStore获取Uri

fun getSaveLocationUri(p0: Context): Uri {
    val savePath = stringPreferencesKey("save_path_uri")
    val flow: Flow<String> = p0.dataStore.data.map { preferences ->
        preferences[savePath] ?: "NOT SET"
    }

    val saveUriString: String
    runBlocking(Dispatchers.IO) {
        saveUriString = flow.first()
    }

    return if (saveUriString != "NOT SET") {
        Uri.parse(saveUriString)
    } else {
        Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toUri()
    }
}

尝试保存文件的代码

val fileNameArr = fullFileName.split(".")
val fileName = fileNameArr[0]

val directory = DocumentFile.fromTreeUri(this, DataStoreHandler.getSaveLocationUri(this))
var newFile = when (fileNameArr.last()) {
    "png" -> directory!!.createFile("image/png", fileName)
    "webp" -> directory!!.createFile("image/webp", fileName)
    "jpg" -> directory!!.createFile("image/jpeg", fileName)
    "jpeg" -> directory!!.createFile("image/jpeg", fileName)
    "gif" -> directory!!.createFile("image/gif", fileName)
    else -> TODO()
}
if (newFile == null) {
    newFile = directory.findFile(fileName + fileNameArr.last())
}
if (newFile == null) {
    Log.e("doc", "somehow it still failed help")
}
val out = FileOutputStream(newFile!!.uri.path)
out.write(bytes)
MediaScannerConnection.scanFile(
    this, arrayOf(newFile.uri.path),
    null
) { path, uri ->
    Toast.makeText(this, "saved", Toast.LENGTH_SHORT).show()
}

问题原因与解决方法

1. Intent权限设置错误

你在Intent中仅计算了权限值,但未赋值给flags属性,且缺少持久化权限,导致重启APP后权限失效,无法访问选中的目录。

修正后的Intent代码:

val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
    flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or 
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
            Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION // 必须添加持久化权限
    putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}

同时在获取返回Uri后,需主动申请持久化权限:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == YOUR_REQUEST_CODE && resultCode == RESULT_OK) {
        data?.data?.let { treeUri ->
            // 持久化权限
            contentResolver.takePersistableUriPermission(
                treeUri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            )
            // 保存Uri到DataStore
            lifecycleScope.launch {
                DataStoreHandler.setSaveLocationUri(this@YourActivity, treeUri)
            }
        }
    }
}

2. DocumentFile.createFile文件名处理问题

createFile的第二个参数建议传入完整带扩展名的文件名,部分设备会忽略MIME类型自动添加的扩展名,导致文件创建失败或名称异常。

修正后的文件创建逻辑:

val fileNameArr = fullFileName.split(".")
val mimeType = when (fileNameArr.last().lowercase()) {
    "png" -> "image/png"
    "webp" -> "image/webp"
    "jpg", "jpeg" -> "image/jpeg"
    "gif" -> "image/gif"
    else -> "application/octet-stream"
}

val directory = DocumentFile.fromTreeUri(this, DataStoreHandler.getSaveLocationUri(this))
var newFile = directory?.createFile(mimeType, fullFileName)

3. 错误使用FileOutputStream访问Content Uri

DocumentFile的Uri是content://协议,不是本地文件路径,不能用FileOutputStream直接打开,必须通过ContentResolver.openOutputStream()获取输出流。

修正后的写入逻辑:

newFile?.let { file ->
    contentResolver.openOutputStream(file.uri)?.use { out ->
        out.write(bytes)
        // 通知媒体库扫描文件
        MediaScannerConnection.scanFile(
            this,
            arrayOf(file.uri.toString()),
            arrayOf(mimeType)
        ) { _, _ ->
            Toast.makeText(this, "保存成功", Toast.LENGTH_SHORT).show()
        }
    } ?: run {
        Log.e("doc", "无法打开输出流")
    }
} ?: run {
    Log.e("doc", "创建或查找文件失败")
}

4. 默认目录兼容性处理

Environment.getExternalStoragePublicDirectory在Android Q及以上已废弃,建议改用MediaStore保存到公共图片目录,或使用应用私有目录(无需权限):

保存到公共图片目录示例

fun saveToPublicPictures(context: Context, bytes: ByteArray, fullFileName: String, mimeType: String) {
    val contentValues = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, fullFileName)
        put(MediaStore.Images.Media.MIME_TYPE, mimeType)
        put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
    }

    val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
    uri?.let {
        context.contentResolver.openOutputStream(it)?.use { out ->
            out.write(bytes)
            MediaScannerConnection.scanFile(context, arrayOf(it.toString()), arrayOf(mimeType)) { _, _ ->
                Toast.makeText(context, "保存成功", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 11:20:57