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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 17:07:46