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.

Thibault Seisel
  • 1,197
  • 11
  • 23
  • `(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. – blackapps Oct 08 '19 at 10:12
  • 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. – Thibault Seisel Oct 08 '19 at 11:54
  • If you can put files in `/storage/emulated/0/Music` then you can certainly delete them your self. Why messing around with the MediaStore? – blackapps Oct 08 '19 at 12:48
  • Because Android 10 introduces new restrictions on files stored on the shared storage. Apps will no longer be able to see all files : https://developer.android.com/training/data-storage/files/external-scoped – Thibault Seisel Oct 08 '19 at 13:00
  • 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. – blackapps Oct 08 '19 at 13:31
  • 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. – Thibault Seisel Oct 08 '19 at 13:42
  • 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. – blackapps Oct 08 '19 at 13:48

2 Answers2

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.

Nikki Borrelli
  • 899
  • 9
  • 21
  • 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. – Thibault Seisel Oct 11 '19 at 09:51
  • Deleting 1 file at a time is so lame. There should be a way do delete them in bulk. – artman Jan 20 '20 at 14:37
  • 2
    @artman 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. – Nikki Borrelli Jan 29 '20 at 18:02
  • "For android 10+, contentResolver.delete will both remove the index AND the xontent itself". This is not true for me. Please [have a look](https://stackoverflow.com/questions/60702967/android-10-how-to-delete-mediastore-item-and-its-associated-data-on-file-syste) – artman Mar 18 '20 at 22:16
  • 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. – Tim Malseed Apr 03 '20 at 07:42
  • 1
    @TimMalseed Found a solution for this? Also have the same problem it just deletes the entry in the MediaStore. – Vince VD Apr 07 '20 at 13:47
  • @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. – Tim Malseed Apr 07 '20 at 23:57
  • 1
    @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. – AndrewBloom Apr 14 '20 at 19:35
  • `contentResolver.delete(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, MediaStore.Video.Media._ID + "=?", ids.toTypedArray())` doesn't work on Android 11 – user25 Nov 20 '20 at 15:30
  • I added android:requestLegacyExternalStorage="true" in my AndroidManifest but still getting RecoverableSecurityException on Android 29(Q). Any solution? – Himanshu Feb 12 '21 at 19:58
  • 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](https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage) 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. – Pilot_51 Apr 13 '21 at 15:13
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()

AndroidDev
  • 1,485
  • 2
  • 18
  • 33