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

