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

Android下无需逐个调用文件选择器,从M3U播放列表获取文件读取权限并读取元数据的实现方案问询

Android下无需逐个调用文件选择器,从M3U播放列表获取文件读取权限并读取元数据的实现方案问询

我现在想做这么个功能:读取一个M3U播放列表文件,自动获取列表里每个音频文件的元数据,但卡在权限这块了。明确说下,我不想给列表里每个文件都弹文件选择器,只选播放列表本身就行,剩下的操作必须是自动完成的。

我当前的播放列表权限获取代码

我用ActivityResultContracts.OpenDocument来获取播放列表的读取权限,还做了持久化权限,代码大概是这样的:

val filePickerLauncher = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.OpenDocument(),
    onResult = { uri: Uri? ->
        uri?.let {
            context.contentResolver.takePersistableUriPermission(
                it,
                Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            )
            context.contentResolver.openInputStream(it)?.use { inputStream ->
                // 读取播放列表文件内容
            }
            // 这里要加下面说的方案1或者方案2的逻辑
        }
    }
)

我想到的两个实现方案

方案一(不太理想的方案):通过MediaStore全局扫描匹配路径

思路是先读取播放列表里的路径,再和MediaStore扫描出来的所有音频文件做路径匹配。我写的MediaStore扫描函数大概是这样的:

fun scanMediaStore(): MutableList<...> {
    val mediaFiles: MutableList<...> = mutableListOf()
    val projection = arrayOf(
        MediaStore.Audio.Media._ID,
        MediaStore.Audio.Media.TITLE,
        MediaStore.Audio.Media.DATA,
        MediaStore.Audio.Media.ARTIST,
        MediaStore.Audio.Media.DURATION,
        MediaStore.Audio.Media.ALBUM_ID,
    )
    val selectionArgs = arrayOf("%.mp3", "%.m4a")
    val selection = selectionArgs.joinToString(" OR ") { "${MediaStore.Audio.Media.DATA} LIKE ?" }
    val sortOrder = null

    fun queryMediaStore(uri: Uri) {
        Log.d("Scanning", "queryMediaStore: $uri")
        app.contentResolver.query(
            uri,
            projection,
            selection,
            selectionArgs,
            sortOrder
        )?.use { cursor ->
            while (cursor.moveToNext()) {
                val getOrThrow: (String) -> Int = { columnName -> cursor.getColumnIndexOrThrow(columnName) }
                val mediaID: Int = cursor.getInt(getOrThrow(MediaStore.Audio.Media._ID))
                val name: String = cursor.getString(getOrThrow(MediaStore.Audio.Media.TITLE))
                val path: String = cursor.getString(getOrThrow(MediaStore.Audio.Media.DATA))
                val artist: String = cursor.getString(getOrThrow(MediaStore.Audio.Media.ARTIST))
                val duration: Int = cursor.getInt(getOrThrow(MediaStore.Audio.Media.DURATION))
                Log.d("MediaFiles", "Found: $path")
                mediaFiles.add(...) // 把扫描到的文件信息加入列表
            }
        }
    }

    queryMediaStore(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI)
    queryMediaStore(MediaStore.Audio.Media.INTERNAL_CONTENT_URI)
    return mediaFiles
}

之后遍历播放列表里的每个路径,和scanMediaStore()返回的列表做匹配:

// 伪代码
for (path in 播放列表内容) {
    MatchAgainst(path, scanMediaStore()) // 匹配路径或文件名
}

这个方案的问题在于MediaStore扫描可能不全,而且相对路径的匹配很容易出问题,所以我更倾向于方案二,但目前卡壳了。

方案二(更优但未实现的方案):通过路径获取MediaID再获取元数据

思路是读取播放列表里的路径后,先拿到对应的MediaID,再用这个ID去获取元数据,如果MediaStore里没有对应条目再补充获取。我只写了伪代码,核心函数还不知道怎么实现:

// 伪代码
fun getMediaIDFor(path: String): Int {
    // 如何通过路径获取对应的MediaID?
}

fun getMetaDataAndAdd(path: String) {
    // 如何通过路径或MediaID获取元数据?
}

// 遍历播放列表
for (path in 播放列表内容) {
    val itemID = getMediaIDFor(path)
    if (itemID !in scanMediaStore()) { // 检查是否在已扫描列表中
        getMetaDataAndAdd(itemID/path)
    }
}

补充说明:我处理的是三星音乐生成的M3U文件,里面的路径都是相对路径,比如../../.../SDCARDID/foo/bar/song.mp3这种格式。

我尝试过但失败的方法

  1. 直接用路径读取文件:触发权限错误
  2. 尝试通过MediaStore查询构造的URI:同样报权限错误
    我还写了个解析相对路径生成URI的函数,但用这个URI去查询还是没权限:
fun resolveRelativeUri(baseUri: Uri, relativePath: String): Uri {
    val baseDocId = DocumentsContract.getDocumentId(baseUri)
    val baseParts = baseDocId.split(":", limit = 2)
    val storageRoot = baseParts[0]
    val baseDir = baseParts.getOrNull(1) ?: ""
    val normalizedPath = File("/$baseDir", relativePath).normalize().path.removePrefix("/")
    val resolvedDocId = "$storageRoot:$normalizedPath"
    return DocumentsContract.buildDocumentUri(
        "com.android.externalstorage.documents",
        resolvedDocId
    )
}

// 尝试用这个URI获取元数据的函数
suspend fun getSongFromUri(uri: Uri): ...? = withContext(Dispatchers.IO) {
    Log.d("ViewModel", "Trying to read $uri")
    val projection = arrayOf(
        MediaStore.Audio.Media._ID,
        MediaStore.Audio.Media.TITLE,
        MediaStore.Audio.Media.DATA,
        MediaStore.Audio.Media.ARTIST,
        MediaStore.Audio.Media.DURATION
    )
    app.contentResolver.query(uri, projection, null, null, null)?.use {
        // 处理查询结果
    }
}

想请教下,方案二的核心函数该怎么实现?或者有没有其他更好的方法,能在只选择一次播放列表的情况下,自动获取里面所有音频文件的读取权限并拿到元数据?

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 11:14:49