使用SAF与MediaStore时调用getVolumeName抛出参数异常问题
解决SAF与MediaStore结合的卷名异常及实践方案
问题背景
在API 29+环境下,使用SAF(存储访问框架)选择可读写的媒体文件夹后,结合MediaStore操作文件时遇到两个问题:
- 调用
MediaStore.getVolumeName(uriFolder)抛出IllegalArgumentException,提示缺失卷名 - 需要手动解析SAF URI获取
relativePath,操作繁琐
SAF返回的示例文件夹URI:
- 主存储:
content://com.android.externalstorage.documents/tree/primary%3AMUSIC - 外置SD卡:
content://com.android.externalstorage.documents/tree/18F0-2D1C%3AMusic
异常原因
MediaStore.getVolumeName()仅支持解析MediaStore自身的content URI(如content://media/external_primary/audio/media),而SAF返回的是DocumentsProvider的tree URI,两者结构完全不同,直接传入会触发参数校验异常。
解决方案
1. 正确解析SAF URI获取卷名
通过DocumentsContract工具类解析SAF的tree URI,提取卷名:
- 主存储的
primary对应MediaStore常量VOLUME_EXTERNAL_PRIMARY - 外置SD卡直接取URI中对应的设备ID
import android.net.Uri import android.provider.DocumentsContract import android.provider.MediaStore fun getVolumeNameFromSafTreeUri(uri: Uri): String { val docId = DocumentsContract.getTreeDocumentId(uri) val segments = docId.split(":") return if (segments[0] == "primary") { MediaStore.VOLUME_EXTERNAL_PRIMARY } else { segments[0] } }
2. 可靠获取RelativePath
同样通过DocumentsContract解析docId,直接提取冒号后的路径部分,无需手动处理URI编码:
fun getRelativePathFromSafTreeUri(uri: Uri): String { val docId = DocumentsContract.getTreeDocumentId(uri) return docId.split(":").getOrNull(1) ?: "" }
3. SAF与MediaStore结合的顺畅实践
结合上述解析方法,实现媒体文件的插入操作示例:
import android.content.ContentValues import android.content.Context import android.net.Uri import android.provider.MediaStore import java.io.OutputStream fun insertAudioToSelectedFolder( context: Context, safTreeUri: Uri, fileName: String, audioContent: ByteArray ) { // 解析卷名和相对路径 val volumeName = getVolumeNameFromSafTreeUri(safTreeUri) val relativePath = getRelativePathFromSafTreeUri(safTreeUri) // 构建MediaStore的内容URI val mediaUri = MediaStore.Audio.Media.getContentUri(volumeName) // 配置文件元数据 val contentValues = ContentValues().apply { put(MediaStore.Audio.Media.DISPLAY_NAME, fileName) put(MediaStore.Audio.Media.RELATIVE_PATH, relativePath) put(MediaStore.Audio.Media.MIME_TYPE, "audio/mpeg") put(MediaStore.Audio.Media.IS_PENDING, 1) // 标记为待处理,避免其他应用干扰 } // 插入文件并写入内容 context.contentResolver.insert(mediaUri, contentValues)?.let { insertedUri -> context.contentResolver.openOutputStream(insertedUri)?.use { outputStream: OutputStream -> outputStream.write(audioContent) } // 完成写入后取消待处理标记 val updateValues = ContentValues().apply { put(MediaStore.Audio.Media.IS_PENDING, 0) } context.contentResolver.update(insertedUri, updateValues, null, null) } }
4. 替代File类的操作建议
- 写入/新建文件:优先用上述MediaStore方式,自动适配存储权限规则
- 读取/修改已有文件:可通过SAF的
DocumentFile.fromTreeUri()获取文件夹的DocumentFile实例,直接操作子文件;也可以通过MediaStore查询匹配的文件URI进行操作 - 权限兼容:SAF的文件夹权限是长期授权,无需动态申请READ_EXTERNAL_STORAGE/WRITE_EXTERNAL_STORAGE(API 33+),结合MediaStore可覆盖绝大多数媒体文件操作场景
内容的提问来源于stack exchange,提问作者user924
相关产品推荐
相关产品推荐

