Im trying to make a simple android mediaplayer app that can be controlled from a distance. At the moment I'm trying to fix the issue of sending all the information on artists/albums/songs that are on the phone. At the moment I'm retrieving all the information as such:
private val contentResolver = activity.contentResolver!!
fun getAll():Set<Album>{
val res = mutableSetOf<Album>()
val cursor = contentResolver.query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
arrayOf(
MediaStore.Audio.Albums.ALBUM,
MediaStore.Audio.Albums.ALBUM_ART,
MediaStore.Audio.Albums.NUMBER_OF_SONGS,
MediaStore.Audio.Albums.ARTIST)
,null,null)
if(cursor!!.moveToFirst())
do {
res.add(Album().apply {
name = cursor.getString(0)
if (!cursor.getString(1).isNullOrEmpty())
albumArtUri = Uri.parse(cursor.getString(1))
songCount = cursor.getInt(2)
artist = Artist().apply {
name = cursor.getString(3)
}
})
cursor.moveToNext()
}while (!cursor.isAfterLast)
cursor.close()
return res
}
Seeing that I'm using a cursor, I thought I was working with a kind of database (SQLite or so) As you can see, this is a lot of code for just a set of objects with little information; the album objects created don't have the songs in them. For this you'd need to start a new query, starting and a new URI. Now I thought I could use an ORM. So I can actually fill the album objects with a list of songs and so on. I decided to try Jetbrains Exposed, typed:
val database = Database.connect(....)
and I'm at a loss, I don't know how to connect to this the database. I can't seem to find any examples on how to start with this.
Exposed is for JDBC. ContentResolver is not using JDBC, and the Cursor is not an object from JDBC. In general, Android does not use JDBC, in apps or at the OS level.
Related
I'm trying to update my old music player to support Android 10 and 11 (and 12 soon)
My code to rename and delete a playlist broke due to changes in security I guess ?? Here is what used to work :
private fun renamePlaylist(resolver: ContentResolver, playlist: Playlist, newName: String) {
val uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI
val selection = "${MediaStore.Audio.Playlists._ID}=${playlist.id}"
val c = ContentValues()
c.put(MediaStore.Audio.Playlists.NAME, newName)
resolver.update(uri, c, selection, null)
}
and
fun deletePlaylist(resolver: ContentResolver, id: Long) {
val uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI
val selection = "${MediaStore.Audio.Playlists._ID}=$id"
resolver.delete(uri, selection, null)
}
I have tested again on a Android 9 device and it works perfectly. Now I have this error
java.lang.IllegalArgumentException: Movement of content://media/external/audio/playlists which isn't part of well-defined collection not allowed
After digging the Android source code it looks like my URI is of type AUDIO_PLAYLISTS and it expects a type AUDIO_PLAYLISTS_ID. So i tried to cheat and append the playlist id to the URI to match the expected type. Now it doesn't crash but the playlist is not deleted either, nothing happens
Thanks for your help
Use the uri fromMediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) for Q and above and MediaStore.Audio.Media.EXTERNAL_CONTENT_URI for below Q
This is because of the recent scoped storage changes
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.
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.
Updating metadata of audio file in media store is not working in Android Q OS, it works in all other OS.
I am using content provider with uri specified as MediaStore.Audio.Media.EXTERNAL_CONTENT_URI. It is working fine in all below Android Q device. Below is the code that I am using to update track metadata.
ContentValues cv = new ContentValues();
ContentResolver resolver = getContentResolver();
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
cv.put(MediaStore.Audio.Media.TITLE, newTitle);
cv.put(MediaStore.Audio.Media.ALBUM, newAlbumName);
cv.put(MediaStore.Audio.Media.ARTIST, newArtistName);
int rowsUpdated = resolver.update(uri, cv,
MediaStore.Audio.Media._ID + " = ? ", new String[]{audioId});
For Android Q device, rowsUpdated is always 0 with no exception.
How are other music player updating tracks metadata in Android Q ?
Finally, it took some time but I figured that out.
First, you need to obtain access to file. Here you can read about that
Next, I found out that to update title or artist fields (maybe others to, I didn't test them) you need to set column MediaStore.Audio.Media.IS_PENDING value to 1. Like that:
val id = //Your audio file id
val values = ContentValues()
values.put(MediaStore.Audio.Media.IS_PENDING, 1)
val uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id)
contentResolver.update(uri, values, null, null)
And then you can edit fields that you need. Also to end the update process set MediaStore.Audio.Media.IS_PENDING to 0 again:
val id = //Your audio file id
val title = //New title
val artist = //New artist
val values = ContentValues()
values.put(MediaStore.Audio.Media.IS_PENDING, 0)
values.put(MediaStore.Audio.Media.TITLE, title)
values.put(MediaStore.Audio.Media.ARTIST, artist)
val uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id)
contentResolver.update(uri, values, null, null)
So in one function, it would look like this:
#RequiresApi(value = android.os.Build.VERSION_CODES.Q)
fun updateMetadata(contentResolver: ContentResolver, id: Long, title: String, artist: String) {
val uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id)
val values = ContentValues()
values.put(MediaStore.Audio.Media.IS_PENDING, 1)
contentResolver.update(uri, values, null, null)
values.clear()
values.put(MediaStore.Audio.Media.IS_PENDING, 0)
values.put(MediaStore.Audio.Media.TITLE, title)
values.put(MediaStore.Audio.Media.ARTIST, artist)
contentResolver.update(uri, values, null, null)
}
It's written in Kotlin but I think you will figure out how to do that in java.
UPDATE
By updating MediaStore you don't updating real file at any android version. That means, if a file would be updated (for example: renamed) and/or scanned by MediaScannerConnection your changes will be lost. This answer is right.
Using Android Q and beyond you have to first get the file
i.e
resolver.openInputStream(uri)?.use { stream -> outputFile.copyInputStreamToFile(stream) }
return outputFile.absolutePath
Helper Function
private fun File.copyInputStreamToFile(inputStream: InputStream?) {
this.outputStream().use { fileOut ->
inputStream?.copyTo(fileOut)
}
}
Then alter the metadata via a third party, I use J Audio Tagger
Then over write the old file
// From https://developer.android.com/reference/android/content/ContentProvider
// String: Access mode for the file. May be
// "r" for read-only access,
// "w" for write-only access (erasing whatever data is currently in the file),
// "wa" for write-only access to append to any existing data,
// "rw" for read and write access on any existing data, and
// "rwt" for read and write access that truncates any existing file. This value must never be null.
mContext.application.contentResolver.openOutputStream(uri, "w")?.use { stream ->
stream.write(file.readBytes())
}
This works fine when the file was created by your app
I've been updating meta data in the MediaStore through a ContentResolver, but this no longer works with Android Q (API 29). The following code gives me a warning, and the description is not updated:
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DESCRIPTION, "Some text");
res = getContext().getContentResolver().update(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values,
MediaStore.Images.Media._ID + "= ?", new String[]{sImageId});
android.process.media W/MediaProvider: Ignoring mutation of
description from com.example.android.someapp.app
This Medium post describes how Google has changed the API for accessing and updating files, but what about updating just the meta data? The warning seems to tell me Google no longer wants to allow third party apps to use the MediaStore, and I also found where the warning comes from:
https://android.googlesource.com/platform/packages/providers/MediaProvider/+/master/src/com/android/providers/media/MediaProvider.java#2960
Writing this on the fly, so I apologize for the code sample. This is NOT real code, it's something I wrote in a plain text editor on the fly. No compile checking, couldn't remember all the exact class and method names, etc. It's just a written concept of what I'm trying to do, I'm looking for feedback on the broader concepts.
I'm working on retrieving a list of contacts from the content provider. I want to be able to filter the results based on the contact's account name. the user will be presented with all available accounts, and will select which ones are to be used, and then that will be used in the retrieval method.
The thing is, the account name is in RawContacts, and the rest of the info I want (display name, lookupID) is in Contacts. I know that ContactsContract.Contacts.Entity is the shortcut to access all of this, so this code sample is what I'm planning to do.
Again, this is written on the fly with no IDE or looking up methods or anything. I'm sure my syntax is bad in many places, but this shows the concept I'm trying to do.
private static final URI URI = ContactsContract.Contacts.URI;
private static final String[] FIRST_PROJECTION = new String[]{
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.LOOKUP_KEY
};
private String[] acceptedAccountNames = {Accepted Account Names Will Go Here (dynamic)};
private static final String[] SECOND_PROJECTION = new String[]{
ContactsContract.Contacts.Entity.ACCOUNT_NAME //This is whatever the entity -> RawContacts field name would be
};
public List<Contact> loadContacts(Context context){
List<Contact> contacts = new ArrayList<>();
ContentProvider provider = context.getContentProvider();
Cursor contactsCursor = provider.query(URI, FIRST_PROJECTION, null, null);
contactsCursor.movetoFirst();
while(!contactsCursor.isAtLast()){
String name = contactsCursor.getString(contactsCursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
long lookupKey = contactsCursor.getLong(contactsCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY));
Uri idUri = Uri.makeWithId(URI, lookupKey);
Uri entityUri = Uri.makeWithTableName(idUri, "entity");
Cursor contactEntityCursor = provider.query(entityUri, SECOND_PROJECTION, null, null);
contactEntityCursor.moveToFirst();
String accountName = contactEntityCursor.getString(contactEntityCursor.getColumnIndex(ContactsContract.Contacts.Entity.ACCOUNT_NAME));
if(Arrays.asList(acceptedAccountNames).contains(accountName)){
Contact contact = new Contact(lookupKey, name);
contacts.add(contact);
}
contactsCursor.moveToNext();
}
return contacts;
}
As you can see, I create a cursor while looping over another cursor. I'm essentially creating a new cursor for each contact in the list.
My question is twofold:
1) What would be the performance implications of this? With a large enough list, would this severely hurt app performance?
2) Is there a better way to do this? As in, a way to do this in a single query, getting all the data I'm looking for in the cursor.
Thanks so much in advance.
I'm using Xamarin.Mobile Component for Android to fetch contacts using the code:
var book = new AddressBook (Activity) {PreferContactAggregation = true};
var contData = data.Data;
var cur = Activity.ManagedQuery (contData, null, null, null, null);
Contact myContact = null;
var lookupKeyList = new List<string> ();
while (cur.MoveToNext ()) {
lookupKeyList.Add (cur.GetString (cur.GetColumnIndexContactsContract.Contacts.InterfaceConsts.LookupKey)));
}
myContact = book.Where (c => c.Id == lookupKeyList [0]).First ();
This code is part of picking a contact from the phone book and receiving the data on OnActivityResult method.
Unfortunately, this code is taking up too much time on some devices and is instantaneous on others. I guess its related to Contact Aggregation but I am not sure. Any pointers?
Sounds like an Android issue, not necessarily a Xamarin issue. Take a look at the question Getting name and email from contact list is very slow. One of the things this answer does is use a Projection in the query to get all the columns in one go.