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

