Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get URI or ID for Media from RELATIVE_PATH + DISPLAY_NAME

I'm trying to simply add Audio Files. My solution mostly works. But where it does not work is when the file already exists in the MediaStore. The once I've looked at more closely also only exist in the MediaStore there is no file on the device at the location.

val values = ContentValues().apply {
   put(MediaStore.Audio.Media.RELATIVE_PATH, libraryPart.rootFolderRelativePath) // JDrop/1/1
   put(MediaStore.Audio.Media.DISPLAY_NAME, remoteLibraryEntry.getFilename()) //12.mp3
   put(MediaStore.Audio.Media.IS_PENDING, 1)
   if(mimeType != null)
       put(MediaStore.Audio.Media.MIME_TYPE, mimeType) // audio/mpeg3
}

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

var uri = ctx.contentResolver.insert(collection, values) // returns null for around 300/2000 files consistently

Logcat outputs the following when trying to insert that new file.

2020-01-24 22:27:33.724 4015-7707/? E/SQLiteDatabase: Error inserting title_key= bucket_display_name=1 owner_package_name=shio.at.jdrop parent=79657 volume_name=external_primary title_resource_uri=null _display_name=12.mp3 mime_type=audio/mpeg3 _data=/storage/emulated/0/Music/JDrop/1/1/12.mp3 title= group_id=1569 artist_id=322 is_pending=1 date_added=1579901253 album_id=2958 primary_directory=Music secondary_directory=JDrop bucket_id=687581593 media_type=2 relative_path=Music/JDrop/1/1/ from {P:30220;U:10165}
android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: files._data (code 2067 SQLITE_CONSTRAINT_UNIQUE)
    at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
    at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:879)
    at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:790)
    at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:88)
    at android.database.sqlite.SQLiteDatabase.insertWithOnConflict(SQLiteDatabase.java:1639)
    at android.database.sqlite.SQLiteDatabase.insert(SQLiteDatabase.java:1494)
    at com.android.providers.media.MediaProvider.insertFile(MediaProvider.java:3050)
    at com.android.providers.media.MediaProvider.insertInternal(MediaProvider.java:3452)
    at com.android.providers.media.MediaProvider.insert(MediaProvider.java:3240)
    at android.content.ContentProvider$Transport.insert(ContentProvider.java:325)
    at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:164)
    at android.os.Binder.execTransactInternal(Binder.java:1032)
    at android.os.Binder.execTransact(Binder.java:1005)

So files._data would mean the file already exists in the MediaStore. There is no file at JDrop/1/1/12.mp3, it's just in the MediaStore and I need to somehow get rid of it or get an OutputStream for the existing MediaStore entry and update it accordingly.

I've tried to query for the ID in the MediaStore without success using the following code. Either finding out the ID or the URI would be fine. Furthermore MediaStore.Audio.Media.DATA is deprecated as of SDK 29. So I would like to query it without using that.

if(uri == null) {
    val id: Long = ctx.contentResolver.query(
            collection,
            arrayOf(BaseColumns._ID),
            "${MediaStore.Audio.Media.RELATIVE_PATH}=? AND ${MediaStore.Audio.Media.DISPLAY_NAME}=?",
            arrayOf(libraryPart.rootFolderRelativePath, remoteLibraryEntry.getFilename()),
            null)?.use {
        if (it.moveToNext())
            it.getLong(it.getColumnIndex(BaseColumns._ID))
        else null
    } ?: return false

    uri = Uri.withAppendedPath(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id!!.toString())
}

EDIT 1 (Querying _data)

I also now tried to query against _data with the hardcoded path that I know I cannot insert, without much success.

val id: Long = ctx.contentResolver.query(
            collection,
            arrayOf(BaseColumns._ID),
            "${MediaStore.Audio.Media.DATA}=?",
            arrayOf("/storage/emulated/0/Music/JDrop/1/1/12.mp3"),
            null)?.use {
        if (it.moveToNext())
            it.getLong(it.getColumnIndex(BaseColumns._ID))
        else null
    } ?: return false

Also gets back null and returns false.

EDIT 2 (Querying everything and see what it returns)

I tried to do a little test query against the entier collection as suggested.

class TestQueryObject(val id: Long, val relativePath: String, val displayName: String)
val results = mutableListOf<TestQueryObject>()

ctx.contentResolver.query(
        collection,
        arrayOf(MediaStore.Audio.Media._ID, MediaStore.Audio.Media.RELATIVE_PATH, MediaStore.Audio.Media.DISPLAY_NAME),
        null,
        null,
        null)?.use {
    while (it.moveToNext()) {
        results.add(TestQueryObject(
                id = it.getLong(it.getColumnIndex(MediaStore.Audio.Media._ID)),
                relativePath = it.getString(it.getColumnIndex(MediaStore.Audio.Media.RELATIVE_PATH)),
                displayName = it.getString(it.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME))
        ))
    }
}

var find12 = results.find { it.displayName == "12.mp3" }

It returns a list of 2557 entries. As an example for the first one the name is "5.mp3" the id is 79658 the relative path is "Music/JDrop/1/1". There is a Music/ in front of it that I did not know of. But find12 is still null.

EDIT 3 (Additional thoughts that may or may not be important)

It might also be worth noting that it does not happen on an Android emulator that I created with android 10. But it does on my OnePlus 6 with around those 300'ish files and works for all the rest. I already was using the Program with Android 9 and then upgraded to Android 10. I've read somewhere that files from Android 9 when you upgrade to 10 might be considered orphaned as opposed to belonging to your appllication (at least that is what happens when you uninstall the app that created them). So I may just no longer have access to the mediastore entries I'm looking for? However, i also thought. reading media is accessable to any app now without ANY permission. So if it's an access problem it should fail when trying to write to it. Not when reading or finding it.

Furthermore as @CommonsWare mentioned IS_PENDING may still be set to 1. I do set it back to 0 in the code below what I posted. However, that code may never be executed whenever I close the program while debugging as 9/10 times it's going to be at the part where it downloads and writes the file as that takes by far the longest time of anything happening in the program.

like image 209
Max Avatar asked Sep 07 '25 04:09

Max


1 Answers

I figured out the problem now. Thank's @CommonsWare to mention the pending bit. It might have taken me much longer to figure it out.

There is an method Uri MediaStore.setIncludePending(Uri uri) that you give your uri in and you get an uri back with witch you can query with the pending items included.

Using this new Uri I got from this method returns my find12 thing successfully!

val collection = MediaStore.Audio.Media
    .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
        
var uri = ctx.contentResolver.insert(collection, values)

if(uri == null) {

    class TestQueryObject(val id: Long, val relativePath: String, val displayName: String)
    val results = mutableListOf<TestQueryObject>()

    ctx.contentResolver.query(
            MediaStore.setIncludePending(collection),
            arrayOf(MediaStore.Audio.Media._ID, MediaStore.Audio.Media.RELATIVE_PATH, MediaStore.Audio.Media.DISPLAY_NAME),
            null,
            null,
            null)?.use {
        while (it.moveToNext()) {
            results.add(TestQueryObject(
                    id = it.getLong(it.getColumnIndex(MediaStore.Audio.Media._ID)),
                    relativePath = it.getString(it.getColumnIndex(MediaStore.Audio.Media.RELATIVE_PATH)),
                    displayName = it.getString(it.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME))
            ))
        }
    }

    var find12 = results.find { it.displayName == "12.mp3" }
}

Now I can start to make this actually work again.


Edit Android11

It appears on Android11 MediaStore.setIncludePending(collection) is now deprecated and we are instead supposed to use a different query method that accepts a bundle.

So it becomes something like this

val queryCollection =
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
            MediaStore.setIncludePending(collection)
        else collection

val queryProjection = arrayOf(MediaStore.Audio.Media._ID)

val querySelection = "${MediaStore.Audio.Media.RELATIVE_PATH}=? AND ${MediaStore.Audio.Media.DISPLAY_NAME}=?"
val querySelectionArgs = arrayOf("someRelativePath", "someFilename")

val queryBundle = Bundle().apply {
    putString(ContentResolver.QUERY_ARG_SQL_SELECTION, querySelection)
    putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, querySelectionArgs)
}

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
     queryBundle.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE)

ctx.contentResolver.query(queryCollection, queryProjection, queryBundle, null)?.use {
    ... Do something
}

Those variables to include the pending items in the bundle are not available prior to android 11.

It might be a better idea to just enable legacy storage for android 10 support and tread it like android < 10 and only implement scoped storage on android 10. The option is ignored on android 11, but still works if the app the runs on android 10.

like image 151
Max Avatar answered Sep 08 '25 19:09

Max