Scoped Storage: how to delete multiple audio files via MediaStore?
Asked Answered
P

2

9

I'm trying to delete audio files from the device's external storage (for example in the /storage/emulated/0/Music folder). After analyzing the MediaStore sample, I ended up with the following solution for API 28 and earlier:

fun deleteTracks(trackIds: LongArray): Int {
    val whereClause = buildWildcardInClause(trackIds.size) // _id IN (?, ?, ?, ...)
    return resolver.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, whereClause, Array(trackIds.size) { trackIds[it].toString() })
}

I noticed that the above code only deletes tracks from the MediaStore database but not from the device (files are re-added to the MediaStore after rebooting). So I modified that code to query the Media.DATA column, then used that information to delete the associated files. This works as intended.

But now that Android Q introduced Scoped Storage, Media.DATA (and Albums.ALBUM_ART) are now deprecated because the app may not be able to access those files. ContentResolver.openFileDescriptor can only be used to read files, not delete them.

Then what is the recommended way to delete tracks as of Android Q ? The sample does not show how to delete multiple files from the MediaStore, and MediaStore.Audio.Media seems to work differently than MediaStore.Images.Media.

Pt answered 8/10, 2019 at 9:41 Comment(7)
(that were copied to the device from USB) from the external storage. It is unclear where those files whould reside after having been copied. You meen TO external storage? Where exactly? And from USB is unclear too until you explain how exactly.Vivie
Those are mp3 files that were uploaded to the /storage/emulated/0/Music folder. I want to delete them programmatically from my app. I've edited the question to make it clearer.Pt
If you can put files in /storage/emulated/0/Music then you can certainly delete them your self. Why messing around with the MediaStore?Vivie
Because Android 10 introduces new restrictions on files stored on the shared storage. Apps will no longer be able to see all files : developer.android.com/training/data-storage/files/…Pt
You start about something that does not matter here. You said you were able to put files in /storage/emulated/0/Music. Well if so then you 'saw' that folder and you can delete those files again. Please elaborate.Vivie
I did not create those files programmatically with my app ; the user did by pluging its phone to a computer. So my app cannot see that folder due to Scoped Storage restrictions.Pt
Your app, specially if it is for Android.Q can see nearly all on primary partion and microSD card. And can read and write all. Just use the Storage Access Framework. Let the user select that directory with ACTION_OPEN_DOCUMENT_TREE or individual files with ACTION_OPEN_DOCUMENT.Vivie
L
17

There are two ways to do this in Android 10, which depends on the value of android:requestLegacyExternalStorage in the app's AndroidManifest.xml file.

Option 1: Scoped Storage enabled (android:requestLegacyExternalStorage="false")

If your app added the content to begin with (using contentResolver.insert) then the method above (using contentResolver.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, whereClause, idsArray)) should work. On Android 10, any content submitted to Media Store by an app can be freely manipulated by that app.

For content the user added, or which belongs to another app, the process is a bit more involved.

To delete a single item from Media Store, don't pass the generic Media Store URI to delete (i.e.: MediaStore.Audio.Media.EXTERNAL_CONTENT_URI), but rather, use the item's specific URI.

context.contentResolver.delete(
    audioContentUri,
    "${MediaStore.Audio.Media._ID} = ?",
    arrayOf(audioContentId)

(I'm pretty sure the 'where' clause there is not necessary, but it made sense to me :)

On Android 10 this will likely throw a RecoverableSecurityException, if your targetSdkVersion is >=29. Inside this exception is an IntentSender which can be started by your Activity to request permission from the user to modify or delete it. (The sender is found in recoverableSecurityException.userAction.actionIntent.intentSender)

Because the user must grant permission to each item, it's not possible to delete them in bulk. (Or, it's probably possible to request permission to modify or delete the entire album, one song at a time, and then use the delete query you use above. At that point your app would have permission to delete them, and Media Store would do that all at once. But you have to ask for each song first.)

Option 2: Legacy Storage enabled (android:requestLegacyExternalStorage="true")

In this case, simply calling contentResolver.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, whereClause, idsArray) not only removes the items from the database, but also deletes the files.

The app needs to be holding WRITE_EXTERNAL_STORAGE permission for this to work, but I tested the code you have in the question and verified that the files were also deleted.

So, for older API versions, I think you need to use contentResolver.delete and unlink the file yourself (or unlink the file and submit a request for it to be rescanned via MediaScannerConnection).

For Android 10+, contentResolver.delete will both remove the index AND the content itself.

Lemuroid answered 10/10, 2019 at 22:10 Comment(11)
Thanks for your detailed answer. However, I find that requesting the user consent for deleting each arbitrary track is not convenient, especially when deleting from a MediaBrowserService (custom action). I'll wait for Android 10 to come to my phone and see how Youtube Music and Google Files have handled that.Pt
Deleting 1 file at a time is so lame. There should be a way do delete them in bulk.Maharashtra
@Maharashtra There will be in the next version of Android. If your app often does bulk operations (deleting or modifying groups of media items), it may be better to use the android:requestLegacyExternalStorage on Android 10, and use the bulk edit/delete operations in the next version.Lemuroid
"For android 10+, contentResolver.delete will both remove the index AND the xontent itself". This is not true for me. Please have a lookMaharashtra
Just to be clear, audioContentUri in this case is retrieved via ContentUris.withAppendedId(Media.EXTERNAL_CONTENT_URI, songId). Also, in my testing, this successfully removes the song from the MediaStore, but the underlying file remains.Odisodium
@TimMalseed Found a solution for this? Also have the same problem it just deletes the entry in the MediaStore.Criminate
@VinceVD nope. The only alternative is obtaining a DocumentFile via the Storage Access Framework (the user will have to pick the directory containing the file, and then you'll have to find the file within that tree). Then deleting the DocumentFile. I didn't do this myself, I just disabled the 'delete' option when using the MediaStore.Odisodium
@NicoleBorrelli Scoped Storage is so full of holes that is unusable. to delete a file you need to check security exception and send intent. I don't find startIntentSenderForResult on a service context, for example. Things should be simple, not insane.Andaman
contentResolver.delete(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, MediaStore.Video.Media._ID + "=?", ids.toTypedArray()) doesn't work on Android 11Fleta
I added android:requestLegacyExternalStorage="true" in my AndroidManifest but still getting RecoverableSecurityException on Android 29(Q). Any solution?Cutting
I made the mistake of thinking android:requestLegacyExternalStorage went in the uses-permission tag for WRITE_EXTERNAL_STORAGE and didn't think twice even after coming across some indirectly conflicting information. It wasn't until I saw this a whole 2 days later that I realized it goes in the application tag, which finally got it working in Android 10. In my defense, I was also working on getting delete to work on other versions of Android in that time.Vanish
S
3

In Android 11 deleting/updating multiple media files is supported with the better and cleaner apis.

Prior to Android 10, we have to delete the physical copy of file by forming File object and also delete the indexed file in MediaStore using ContentResolver.delete() (or) do a media scan on the deleted file which would remove it's entry in MediaStore.

This is how it used to work in below Android 10 os. And would still work the same in Android 10 as well if you had opted out of scoped storage by specifying it in manifest android:requestLegacyExternalStorage="true"

Now in Android 11 you are forced to use scoped storage. If you want to delete any media file which is not created by you, you have to get the permission from the user. You can get the permission using MediaStore.createDeleteRequest(). This will show a dialog by describing what operation users are about to perform, once the permission is granted, android has an internal code to take care of deleting both the physical file and the entry in MediaStore.

private void requestDeletePermission(List<Uri> uriList){
  if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
      PendingIntent pi = MediaStore.createDeleteRequest(mActivity.getContentResolver(), uriList);

      try {
         startIntentSenderForResult(pi.getIntentSender(), REQUEST_PERM_DELETE, null, 0, 0,
                  0);
      } catch (SendIntentException e) { }
    }
}

The above code would do both, requesting the permission to delete, and once permission granted delete the files as well.

And the result you would get it in onActivityResult()

Sepulchral answered 12/11, 2020 at 8:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.