Renaming and deleting a music playlist in Android 10+ - android

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

Related

Android R CameraX with scoped storage getting image column id after capture

The app I am working on needs a new lease on life and one renewing task for Android R and above: is getting the id of an image taken by CameraX from scoped storage. The purpose being to save the id in a datatable for upload later.
With regards to permissions the App only saves files to its own external directory, so no permissions are necessary.
After CameraX has captured the image using the following OutputOptions:
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, imgName)
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + File.separator + "Custom directory")
val outputOptions = ImageCapture.OutputFileOptions.Builder(
activityContext.contentResolver,
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL),
contentValues
).build()
The onImageSaved CameraX callback method from the imageCapture.takePicture object returns an ImageCapture.OutputFileResults:
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri
This is where things get fuzzy; I have the Uri of the saved image. I tried using the Uri to retrieve the image column id by searching for the file name with:
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME
)
val selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?"
return contentResolver.query(
savedUri, // the image capture uri
projection, // query columns
selection, // query
arrayOf(Environment.DIRECTORY_DCIM + File.separator + "Custom directory"), // query arguments
null // order by
)?.use { cursor ->
if (cursor.count > 0) {
while (cursor.moveToNext()) {
val fileName =
cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME))
Log.d(TAG, "index $fileName")
if (fileName.equals(displayName)) {
return#use cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns._ID))
}
}
}
return#use null
}
I consistently get null results. So I tried the following content uri in the place of the savedUri:
val contentUri =
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
This also only gives null results and 0 cursor.count columns. Can someone please point me in the right direction on why the above file queries are giving null results?
Edit 1
After reading blackapps comment I went and read the documentation again. My understanding of scoped storage it seems is definitely lacking. I was using getExternalFilesDir before to get an output path and all was fine until I added getExternalFilesDir(Environment.DIRECTORY_DCIM) which according to the below comment is seen as public. That is when I started to meddle with the above MediaStore api and got tangled into thinking to much or little depending on my point of view.
Edit 2
Some more reading of the App specific sources where it does not state anywhere that getExternalFilesDir method will be removed in Api 32 or 33. So it can be used to even save in public directories. The example given is:
val file = File(context.getExternalFilesDir(
Environment.DIRECTORY_PICTURES), albumName)
Further:
It's important that you use directory names provided by API constants like DIRECTORY_PICTURES. These directory names ensure that the files are treated properly by the system.
Caveat to the above quoted documentation is that this excludes DIRECTORY_DCIM: Why is this not in the documentation?
Edit 3:
A good question to ask when uncertain if you are dealing with scoped storage is whether the files are:
Shareable media files (images, audio files, videos)

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

How to update metadata of audio file in Android Q media store?

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

How do I use jetbrains exposed with android contentResolver/Mediastore

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.

Android's Media Scanner: How do I remove files?

I'm writing an app that removes files that may or may not be listed in any one of the types of media libraries such as music or pictures. While I can use the MediaScannerConnection.scanFile method to add files to the media library there doesn't seem to be any call to notify the service that the file has been removed. Sending it the path of the file that no longer exists doesn't result in the desired behavior either. How should I go about removing items from the library that no longer exist on the Android storage?
I was able to put a method together using bits and pieces from these two questions
What is the String 'volumeName' argument of MediaStore.Audio.Playlists.Members.getContentUri referring to?
How can I refresh MediaStore on Android?
Basically I just run a query on each one of the MediaStore types (Audio, Video and Images) selecting by path and deleting any records I find.
public static void RemoveAllForPaths(String[] paths, Context context)
{
private static final String[] FIELDS = { MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.TITLE };
if(paths == null || paths.length == 0) return;
String select = "";
for(String path : paths)
{
if(!select.equals("")) select += " OR ";
select += MediaStore.MediaColumns.DATA + "=?";
}
Uri uri;
Cursor ca;
uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
ca = context.getContentResolver().query(uri, FIELDS, select, paths, null);
for(ca.moveToFirst(); !ca.isAfterLast(); ca.moveToNext()){
int id = ca.getInt(ca.getColumnIndex(MediaStore.MediaColumns._ID));
uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
context.getContentResolver().delete(uri, null, null);
}
ca.close();
// More of the same just setting the URI to Video and Images
}
I'm not entirely sure how safe this is to do but it's the only solution I've found so far and some initial testing seems to be working. I invite others to submit other answers if anyone has any further information on this approach or a better method for performing this functionality.
Answer of Spencer Ruport is right, but you don't need to query and open a cursor in order to delete.
So for one file that is music file the code is simple like that:
public void DeleteMP3FromMediaStore( Context context, String path )
{
Uri rootUri = MediaStore.Audio.Media.getContentUriForPath( path );
context.getContentResolver().delete( rootUri,
MediaStore.MediaColumns.DATA + "=?", new String[]{ path } );
}
P.S. I wanted to comment answer of Spencer Ruport but don't have enough reputation yet.
Easy as pie: whenever you add a file, let MediaStore ContentProvider knows about it using
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(fileToAddInMediaStore)));
For deletion: just use
getContentResolver().delete(Uri.fromFile(fileToDeleteFromMediaStore), null, null)
The following works well for me. You can delete or add files using this.
MediaScannerConnection.scanFile(
context,
new String[]{fileToDelete, fileToAdd},
null, null);
The available method is to remove the item from library.
This post is detailed expressed how to add into or remove from the Media Library.
http://androidyue.github.io/blog/2014/01/19/scan-media-files-in-android/ Hopes this could help you.

Categories

Resources