Android使用Intent.ACTION_OPEN_DOCUMENT选文件后Retrofit上传失败求助
解决Android文件选择与Retrofit上传的核心问题
你遇到的核心问题是**ACTION_OPEN_DOCUMENT返回的是内容Uri(content://开头),而非文件系统的绝对路径**,直接用uri.path创建File对象肯定会报错——因为这个路径并不对应真实的文件位置。Android 10+官方推荐使用存储访问框架(SAF),我们不需要获取真实文件路径,直接通过Content Uri操作文件流即可完成上传、下载全流程。
一、修复文件上传逻辑(无需转换为File)
我们可以直接通过ContentResolver获取文件的输入流,用这个流创建Retrofit的RequestBody,完全绕开File转换的问题:
修改onActivityResult中的上传代码
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == DEVICE_PATH && resultCode == Activity.RESULT_OK) { data?.data?.let { uri -> lifecycleScope.launch { try { // 1. 从Content Uri解析文件名 val fileName = getFileNameFromUri(this@MainActivity, uri) // 2. 打开文件输入流(use块自动关闭流,避免泄漏) contentResolver.openInputStream(uri)?.use { inputStream -> // 3. 用输入流创建RequestBody val requestFile = inputStream.asRequestBody("application/pdf".toMediaTypeOrNull()) // 4. 构建Multipart请求体 val multipartBody = MultipartBody.Part.createFormData( "input", fileName, requestFile ) // 5. 执行上传 val api_key = "DzkpCKjktggtCT1ZE8bFqca7anmmkpOcg975".toRequestBody("text/plain".toMediaTypeOrNull()) val tool_uid = "PR5".toRequestBody("text/plain".toMediaTypeOrNull()) val result = pdfClient.convert(multipartBody, api_key, tool_uid) Log.d("UPLOAD_SUCCESS", "上传响应: ${result.string()}") } ?: Log.e("UPLOAD_ERROR", "无法打开文件输入流") } catch (ex: Exception) { Log.d("UPLOAD_ERROR", "上传失败: ${ex.message}") } } } } }
新增解析文件名的工具方法
fun getFileNameFromUri(context: Context, uri: Uri): String { var fileName = "unknown_file.pdf" // 通过ContentResolver查询文件显示名称 val cursor = context.contentResolver.query(uri, null, null, null, null) cursor?.use { if (it.moveToFirst()) { val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (nameIndex != -1) { fileName = it.getString(nameIndex) } } } return fileName }
二、为什么之前的方法失效?
ACTION_OPEN_DOCUMENT属于SAF,返回的Uri是系统提供的内容访问入口,不是真实的文件路径,uri.path只是Uri的路径片段,并非文件系统路径。- 你写的
getRealPathFromURI方法依赖MediaStore.Images.Media.DATA字段,这个字段在Android 10+已被废弃,且仅适用于媒体文件,对PDF等非媒体文件不生效。 - Android 10+的Scoped Storage限制了直接操作File的行为,SAF是官方推荐的合规访问方式。
三、补充:下载文件并保存到本地
针对你提到的下载需求,这里提供用Retrofit下载并通过SAF保存文件的示例:
1. 在PDFClient接口中添加下载方法
interface PDFClient { // 保留原上传接口 @POST("api/convert") @Multipart suspend fun convert( @Part input: MultipartBody.Part, @Part("api_key") api_key: RequestBody, @Part("tool_uid") tool_uid: RequestBody ): ResponseBody // 新增下载接口(假设服务器通过fileId返回文件流) @GET("api/download/{fileId}") suspend fun downloadFile(@Path("fileId") fileId: String): ResponseBody }
2. 调用下载并保存文件
private val REQUEST_SAVE_FILE = 400 // 启动SAF保存文件选择器 fun startSaveFilePicker() { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/pdf" putExtra(Intent.EXTRA_TITLE, "converted_file.pdf") // 默认文件名 } startActivityForResult(intent, REQUEST_SAVE_FILE) } // 在onActivityResult中处理保存逻辑 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_SAVE_FILE && resultCode == Activity.RESULT_OK) { data?.data?.let { saveUri -> lifecycleScope.launch { try { // 假设从上传响应中拿到fileId val fileId = "your_file_id_from_upload_response" val responseBody = pdfClient.downloadFile(fileId) // 将ResponseBody写入SAF Uri指向的位置 contentResolver.openOutputStream(saveUri)?.use { outputStream -> responseBody.byteStream().copyTo(outputStream) Log.d("DOWNLOAD_SUCCESS", "文件已成功保存") } ?: Log.e("DOWNLOAD_ERROR", "无法打开文件输出流") } catch (ex: Exception) { Log.d("DOWNLOAD_ERROR", "下载失败: ${ex.message}") } } } } }
四、关键注意事项
- Android 10及以下需申请
READ_EXTERNAL_STORAGE权限,Android 11+无需该权限,SAF会自动处理文件访问权限。 - 若需长期访问选中的文件,可调用
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION),确保应用重启后仍能访问该文件。 - 所有流操作使用
use块,自动关闭流避免内存泄漏。
内容的提问来源于stack exchange,提问作者zoha131
相关产品推荐
相关产品推荐

