用于 Android Q 上的辅助存储的媒体扫描仪

Posted

技术标签:

【中文标题】用于 Android Q 上的辅助存储的媒体扫描仪【英文标题】:Media scanner for secondary storage on Android Q 【发布时间】:2019-09-27 10:51:05 【问题描述】:

随着较新的 ​​android Q,许多事情发生了变化,尤其是在范围存储和逐渐弃用 file:/// URI 的情况下。问题是缺乏关于如何在 Android Q 设备上正确处理媒体文件的文档。

我有一个媒体文件(音频)管理应用程序,但我找不到可靠的方法告诉操作系统我对文件执行了更改,以便它可以更新其 MediaStore 记录。

选项 #1:MediaScannerService

MediaScannerConnection.scanFile(context, new String[] filePath , new String[]"audio/*", new MediaScannerConnection.OnScanCompletedListener() 
    @Override
    public void onScanCompleted(String s, Uri uri) 

    
);
与来自主存储的 file:// URI 一起使用 不适用于来自辅助存储(例如可移动存储)的 file:// URI 不适用于任何 content:// URI

选项 #2:广播

context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
根本不工作 即将弃用

选项 #3:手动插入 MediaStore

AudioFileContentValues 是来自MediaStore.Audio.AudioColumns 的一些列值。

基于file:// URI 的旧方法:

Uri uri = MediaStore.Audio.Media.getContentUriForPath(file_path);
newUri = context.getContentResolver().insert(uri, AudioFileContentValues);
MediaStore.Audio.Media.getContentUriForPath 已弃用 还是不行

基于我可以从documentation 汇总的新方法:

Uri collection = MediaStore.Audio.Media.getContentUri(correctVolume);
newUri = context.getContentResolver().insert(collection, AudioFileContentValues);

其中correctVolume 将是来自主存储的external,而类似于0000-0000 用于辅助存储,具体取决于文件所在的位置。

插入会返回一个内容 URI,例如 content://media/external/audio/media/125,但不会在 MediaStore 中为位于主存储中的文件保留任何记录 插入失败,没有返回 URI 并且 MediaStore 中没有记录

这些或多或少是以前 Android 版本中可用的所有方法,但现在它们都不允许我通知系统我更改了一些音频文件元数据并让 Android 更新 MediaStore 记录。事件虽然选项 #1 部分有效,但这绝不可能是一个有价值的解决方案,因为它显然不支持内容 URI。

是否有任何可靠的方法可以在 Android Q 上触发媒体扫描,尽管文件位于何处?根据 Google 的说法,我们甚至不应该关心文件位置,因为我们很快将只使用内容 URI。 MediaStore 在我看来一直有点令人沮丧,但现在情况更糟了。

【问题讨论】:

“我还没有找到一种可靠的方法告诉操作系统我对文件进行了更改,以便它可以更新其 MediaStore 记录”——如果MediaStore 已经知道内容,我认为您需要在选项#3 中使用update() 而不是insert()insert() 如果您正在创建新内容。 @CommonsWare 在测试中,我正在处理在 MediaStore 中还没有自己的行的音频文件,因为它们一次都没有被扫描过。无论如何,我感谢您的博客以及您如何让社区了解即将发生的 Android 行为变化。 【参考方案1】:

我目前也在为此苦苦挣扎。

我认为,一旦您在 Android Q 上,您想要做的事情就无法再做,因为您无法访问 Q 上的音乐目录。您只能在您创建的目录中创建和访问文件。您没有创建音乐目录。

现在对 Media 的每一次更改都必须发生在 MediaStore 中。所以你事先插入你的音乐文件,然后从 MediaStore 获取一个输出流来写入它。对 Q 的所有更改都应该在 MediaStore 上完成,因此您通知 MediaStore 更改甚至不能再发生,因为您永远不会直接访问文件。

这有一个巨大的问题,因为在旧版本的 Android 中不存在 MediaStore 中使这成为可能的所有新事物。因此,我目前确实认为您将需要两次实施所有内容,遗憾的是。至少如果您想积极影响音乐的保存位置。

这两个 MediaStore 列是 Q 中的新列,在 Q 之前不存在,你可能需要在 Q 中使用

MediaStore.Audio.Media.RELATIVE_PATH 可以影响保存路径。所以我把“Music/MyAppName/MyLibraryName”放在那里,最终将“song.mp3”保存到“Music/MyAppName/MyLibraryName/song.mp3”中 MediaStore.Audio.Media.IS_PENDING 这应该在歌曲仍在编写时设置为 1,然后您可以将其更新为 0。

我现在也开始使用 if 检查 Android 版本来实现两次。这很烦人。我不想这样做。但似乎这是唯一的方法。

我将在此处添加一些代码,说明我如何在 Android.Q 及以下版本中插入音乐。这并不完美。我必须为 Q 指定 MIME 类型,因为 flac 现在会以某种方式变为 .flac.mp3,因为它似乎不太明白。

所以,无论如何,这是我已经更新的部分,可以与 Q 一起使用,之前它会从我 NAS 上的音乐播放器下载音乐文件。该应用程序是用 kotlin 编写的,不确定这对您来说是否有问题。

override fun execute(library : Library, remoteApi: RemoteApi, ctx: Context) : Boolean 

    var success = false

    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) 
        val values = ContentValues().apply 
            put(MediaStore.Audio.Media.RELATIVE_PATH, library.rootFolderRelativePath)
            put(MediaStore.Audio.Media.DISPLAY_NAME, remoteLibraryEntry.getFilename())
            put(MediaStore.Audio.Media.IS_PENDING, 1)
        

        val collection = MediaStore.Audio.Media
                .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)

        val uri = ctx.contentResolver.insert(collection, values)

        ctx.contentResolver.openOutputStream(uri!!).use 
            success = remoteApi.downloadMusic(remoteLibraryEntry, it!!)
        

        if(success) 
            values.clear()
            val songId = JDrop.mediaHelper.getSongId(uri)
            JDrop.db.music.insert(Music(mediaStoreId = songId, remoteId = remoteLibraryEntry.remoteId, libraryId = library.id))
            values.put(MediaStore.Audio.Media.IS_PENDING, 0)
            ctx.contentResolver.update(uri, values, null, null)
         else 
            ctx.contentResolver.delete(uri, null, null)
        
     else 

        val file = File("$library.rootFolderPublicDirectory/$remoteLibraryEntry.getFilename()")

        if(file.exists()) file.delete()

        success = remoteApi.downloadMusic(remoteLibraryEntry, file.outputStream())

        if (success) 
            MediaScannerConnection.scanFile(ctx, arrayOf(file.path), arrayOf("audio/*"))  _, uri ->
                val songId = JDrop.mediaHelper.getSongId(uri)
                JDrop.db.music.insert(Music(mediaStoreId = songId, remoteId = remoteLibraryEntry.remoteId, libraryId = library.id))
            
        
    

    return success

MediaStoreHelper 方法在这里

    fun getSongId(uri : Uri) : Long 

    val cursor = resolver.query(uri, arrayOf(Media._ID), null, null, null)

    return if(cursor != null && cursor.moveToNext()) 
        val idIndex = cursor.getColumnIndex(Media._ID)
        val id = cursor.getLong(idIndex)
        cursor.close()
        id
     else 
        cursor?.close()
        -1
    

当您没有指定 MIME 类型时,它似乎假定 mp3 是 MIME 类型。所以 .flac 文件将保存为 name.flac.mp3,因为如果没有,它会添加 mp3 文件类型并且它认为它是 mp3。它不会为 mp3 文件添加另一个 .mp3。我目前在任何地方都没有 MIME 类型……所以我想我现在就去做吧。

还有一个有用的 google IO 讨论关于作用域/共享存储https://youtu.be/3EtBw5s9iRY

这可能无法回答您的所有问题。果然不适合我。但是,粗略了解他们一开始所做的更改是一个有益的开始。


对于删除和更新文件,如果您在媒体存储条目上调用 delete,则在 Q 上它有点相同,该文件将被删除。在此之前,Q 您还必须手动删除该文件。但是如果你在 Q 上这样做,你的应用程序就会崩溃。因此,您必须再次检查您是否使用 Q 或旧版本的 android 并采取适当的措施。

【讨论】:

以上是关于用于 Android Q 上的辅助存储的媒体扫描仪的主要内容,如果未能解决你的问题,请参考以下文章

Android Q(10) 文件存储适配

主码流 辅助码流说明

如何在android中运行媒体扫描仪

MediaScannerService研究

android 使用媒体扫描仪扫描 SD 卡

Android Q:范围存储中的 SQLite 数据库