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

Android 13设备无法获取文件真实路径 求解决方案

Android 13下URI获取文件真实路径失败的解决方案

问题背景

使用提供的RealPathUtil工具类在Android 13设备上通过URI获取文件真实路径时返回null,导致文件上传功能异常,该工具类在低版本Android设备上可正常工作。

原因分析

Android 13(API 33)对存储访问机制做了以下关键限制:

  • MediaStore的_data字段不再保证返回真实文件路径,部分场景下会直接返回null
  • 针对不同类型的媒体文件,新增了细分的权限(READ_MEDIA_IMAGES/READ_MEDIA_VIDEO/READ_MEDIA_AUDIO),原有的READ_EXTERNAL_STORAGE权限在Android 13上对部分文件类型不再生效
  • 存储访问框架(SAF)返回的URI权限机制更严格,直接解析路径的方式不再适用

解决方案

方案一:适配Android 13的路径解析工具类

修改原RealPathUtil,当无法获取真实路径时,将URI指向的文件复制到应用私有目录,返回私有目录的文件路径:

object RealPathUtil {
    fun getRealPath(context: Context, fileUri: Uri): String? {
        val realPath: String?
        if (Build.VERSION.SDK_INT < 11) {
            realPath = getRealPathFromURI_BelowAPI11(context, fileUri)
        } else if (Build.VERSION.SDK_INT < 19) {
            realPath = getRealPathFromURI_API11to18(context, fileUri)
        } else {
            realPath = getRealPathFromURI_API19Plus(context, fileUri)
        }
        return realPath
    }

    // 保留原有的getRealPathFromURI_BelowAPI11、getRealPathFromURI_API11to18方法不变

    @SuppressLint("NewApi")
    fun getRealPathFromURI_API19Plus(context: Context, uri: Uri): String? {
        val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT

        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
            if (isExternalStorageDocument(uri)) {
                val docId = DocumentsContract.getDocumentId(uri)
                val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
                val type = split[0]
                if ("primary".equals(type, ignoreCase = true)) {
                    return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
                }
            } else if (isDownloadsDocument(uri)) {
                val id = DocumentsContract.getDocumentId(uri)
                return if (id.startsWith("raw:")) {
                    id.substring(4)
                } else {
                    val contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"), id.toLong()
                    )
                    getDataColumnOrCopyToPrivate(context, contentUri, null, null)
                }
            } else if (isMediaDocument(uri)) {
                val docId = DocumentsContract.getDocumentId(uri)
                val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
                val type = split[0]
                var contentUri: Uri? = null
                when (type) {
                    "image" -> contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
                    "video" -> contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
                    "audio" -> contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
                }
                val selection = "_id=?"
                val selectionArgs = arrayOf(split[1])
                return getDataColumnOrCopyToPrivate(context, contentUri, selection, selectionArgs)
            }
        } else if ("content".equals(uri.scheme, ignoreCase = true)) {
            return if (isGooglePhotosUri(uri)) {
                uri.lastPathSegment
            } else {
                getDataColumnOrCopyToPrivate(context, uri, null, null)
            }
        } else if ("file".equals(uri.scheme, ignoreCase = true)) {
            return uri.path
        }
        return null
    }

    fun getDataColumnOrCopyToPrivate(
        context: Context, uri: Uri?, selection: String?,
        selectionArgs: Array<String>?
    ): String? {
        var cursor: Cursor? = null
        val column = "_data"
        val projection = arrayOf(column)
        try {
            cursor = context.contentResolver.query(
                uri!!, projection, selection, selectionArgs, null
            )
            if (cursor != null && cursor.moveToFirst()) {
                val index = cursor.getColumnIndexOrThrow(column)
                val path = cursor.getString(index)
                if (path != null && File(path).exists()) {
                    return path
                }
            }
            return copyUriToPrivateFile(context, uri)
        } finally {
            cursor?.close()
        }
    }

    private fun copyUriToPrivateFile(context: Context, uri: Uri?): String? {
        if (uri == null) return null
        try {
            val inputStream = context.contentResolver.openInputStream(uri) ?: return null
            val fileName = getFileNameFromUri(context, uri) ?: "temp_file_${System.currentTimeMillis()}"
            val outputFile = File(context.filesDir, fileName)
            val outputStream = FileOutputStream(outputFile)
            inputStream.copyTo(outputStream)
            inputStream.close()
            outputStream.close()
            return outputFile.absolutePath
        } catch (e: Exception) {
            e.printStackTrace()
            return null
        }
    }

    private fun getFileNameFromUri(context: Context, uri: Uri): String? {
        var fileName: String? = null
        if (uri.scheme == "content") {
            val cursor = context.contentResolver.query(uri, null, null, null, null)
            cursor?.use {
                if (it.moveToFirst()) {
                    fileName = it.getString(it.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))
                }
            }
        }
        if (fileName == null) {
            fileName = uri.path?.substringAfterLast("/")
        }
        return fileName
    }

    // 保留原有的isExternalStorageDocument、isDownloadsDocument、isMediaDocument、isGooglePhotosUri方法不变
}

方案二:推荐的最佳实践——直接使用URI上传文件

无需获取真实路径,直接通过ContentResolver读取文件流进行上传,这种方式适配所有Android版本,包括Android 13,且避免了路径解析的各种兼容性问题:

fun uploadFile(context: Context, uri: Uri, uploadCallback: (Boolean, String?) -> Unit) {
    Thread {
        try {
            val inputStream = context.contentResolver.openInputStream(uri) ?: run {
                uploadCallback(false, "无法打开文件流")
                return@Thread
            }
            // 替换为你的上传逻辑,示例使用OkHttp
            val mediaType = context.contentResolver.getType(uri) ?: "*/*"
            val requestBody = RequestBody.create(MediaType.parse(mediaType), inputStream.readBytes())
            val request = Request.Builder()
                .url("你的上传接口地址")
                .post(requestBody)
                .build()
            val client = OkHttpClient()
            val response = client.newCall(request).execute()
            uploadCallback(response.isSuccessful, if (response.isSuccessful) "上传成功" else "上传失败:${response.message()}")
            inputStream.close()
        } catch (e: Exception) {
            e.printStackTrace()
            uploadCallback(false, "上传出错:${e.message}")
        }
    }.start()
}

权限注意事项

  • Android 13及以上,访问媒体文件需要申请对应的细分权限:READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO,需在Manifest中声明并动态申请
  • 如果使用存储访问框架(SAF)选择文件,用户选择后会自动授予该URI的临时权限,无需额外申请存储权限

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 12:09:52