Media scanner for secondary storage on Android Q
Asked Answered
I

1

23

With the newer Android Q many things changed, especially with scoped storage and gradual deprecation of file:/// URIs. The problem is the lack of documentation on how to handle media files correctly on Android Q devices.

I have a media file (audio) management application and I could not find yet a reliable way to tell to the OS that I performed a change to a file so that it can update its MediaStore record.

Option #1: MediaScannerService

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

    }
});
  • Works with file:// URIs from primary storage
  • Not works with file:// URIs from secondary storage (such as removable storage)
  • Not works with any content:// URI

Option #2: broadcast

context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
  • Not working at all
  • Soon deprecated

Option #3: manual MediaStore insertion

AudioFileContentValues are some column values from MediaStore.Audio.AudioColumns.

Old method based on file:// URI:

Uri uri = MediaStore.Audio.Media.getContentUriForPath(file_path);
newUri = context.getContentResolver().insert(uri, AudioFileContentValues);
  • MediaStore.Audio.Media.getContentUriForPath is deprecated
  • Still not working

Newer method based on what I could put together from documentation:

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

Where correctVolume would be external from primary storage, while it would be something like 0000-0000 for secondary storage, depending on where the file is located.

  • Insertion returns a content URI such as content://media/external/audio/media/125 but then no record is persisted inside MediaStore for files located in primary storage
  • Insertion fails with no URI returned and no record in MediaStore

These are more or less all the methods available in previous Android versions but none of them now allow me to notify the system that I changed some audio file metadata and to get Android to update MediaStore records. Event though option #1 is partially working, this could never be a valuable solution because it's clearly not supporting content URIs.

Is there any reliable way to trigger media scan on Android Q, despite where the file is located? We shouldn't even care about file location, according to Google, since we will soon only use content URIs. MediaStore has always been a little frustrating in my opinion, but now the situation is pretty worse.

Inconceivable answered 9/5, 2019 at 14:52 Comment(2)
"I could not find yet a reliable way to tell to the OS that I performed a change to a file so that it can update its MediaStore record" -- if the MediaStore already knows about the content, I would think that you need to use update() rather than insert() in your Option #3. insert() would be if you are creating a new piece of content.Prier
@Prier in the tests, I was working on audio files that have not yet their own row in the MediaStore because they haven't been scanned neither once. Anyway I appreciate your blog and how you keep the community updated about upcoming Android behavior changes.Inconceivable
C
5

I'm also currently struggling with that.

I think what you want to do you cannot do any longer once you are on Android Q, because you are not allowed to access the Music directory on Q. You are only allowed to create and access files in directories you created. You did not create the music directory.

Now every change to the Media has to happen threw the MediaStore. So you insert your Music file beforehand and then get an outputstream from the MediaStore to write to it. All the changes on Q on Media should be done threw the MediaStore hence you informing the MediaStore of changes cannot even occur anymore, because you never directly access the File.

This has one giant caviat in that all the new things in MediaStore that make that possible do not exist in older versions of Android. So I do currently believe that you will need to implement everything twice, sadly. At least if you want to actively influences where your music is saved to that is.

Those two MediaStore columns are new in Q and do not exist before Q, witch you'll probably need to use in Q

  • MediaStore.Audio.Media.RELATIVE_PATH with that you can influence the path where it's saved. So I put "Music/MyAppName/MyLibraryName" there and that will end up saving "song.mp3" into "Music/MyAppName/MyLibraryName/song.mp3"
  • MediaStore.Audio.Media.IS_PENDING this you should be setting to 1 while the song is still being written and then afterwards you can update it to 0.

I've also now started to implement things twice with if checks for Android versions. It's annoying. I don't want to do it. But it seems like that's the only way.

I'm just gonna put a bit of code here on how I managed inserting music on Android.Q and below. It's not perfect. I have to specify the MIME type for Q, because flacs would now become .flac.mp3 somehow, because it does not quite seem to get that.

So, anyways this is a part that I have updated already to work with Q and before, it downloads a Music file from a music player on my NAS. The app is written in kotlin, not sure if that's a problem for you.

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
}

And the MediaStoreHelper Method being this here

    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
    }
}

One thing when you do not specify the MIME type it seems to assume mp3 is the MIME type. So .flac files would get saved as name.flac.mp3, because it adds the mp3 file type if there is none and it thinks it's a mp3. It does not add another .mp3 for mp3 files. I don't currently have the MIME type anywhere... so I'm gonna go ahead and do this now, I guess.

There is also a helpful google IO talk about scoped/shared storage https://youtu.be/3EtBw5s9iRY

That probably won't answer all of your questions. It sure enough didn't for me. But it was a helpful start to have a rough idea what they even did change to begin with.


For deleting and updating files its kinda the same on Q if you call delete on a mediastore entry, the file will be deleted. Before, Q you have to manually delete the file also. But if you do that on Q your app will crash. So again you have to check wether or not youre on Q or an older version of android and take appropriate actions.

Critchfield answered 20/7, 2019 at 19:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.