My app creates playlists in the android mediastore. All is well for api's including 28 however, api 29 seems to require additional permissions.
Inserting a new playlist name and id works without issue. When it comes to inserting track id and play order, an access permission exception is thrown.
In verifying the Uri, i found that when resolver.insert for API 29 the exception error is:
java.lang.SecurityException: myapp_name has no access to content://media/external_primary/audio/media/146
The code:
Uri exturi = MediaStore.Audio.Playlists.Members.getContentUri("external", playlist_id);
// exturi : content://media/external/audio/playlists/227/members
// values : audio_id=146 play_order=0
values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, play_order);
values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audio_id);
try {
resolver.insert(exturi, values);
} catch (Exception e) {
e.printStackTrace();
}
Strange thing is that although inserting a new playlist into Mediastore works but adding tracks (track_id, play order) gives an access permission error
How to resolve this exception error for API 29?
Update Feb 2021:
a small step forward, I am pretty sure I need to get Documenturi for the original uri but still gives me the access error. So the issue does not lie with accessing the tracks but with the uri itself.
doc_uri = MediaStore.getDocumentUri(context,playlist_members_uri);
java.lang.SecurityException: com.flyingdutchman.newplaylistmanager has no access to content://media/external/audio/playlists/130/members
I think this is an Android 10 bug, so I've filed a report here: https://issuetracker.google.com/issues/147619577 (includes instructions for an emulator test case to reproduce it if that interests you). Please consider starring it to let the Android team know that it affects you.
From what I can tell, it only affects files on 'external' storage, like sdcards mounted on /storage/XXXX-XXXX
In the meantime, the only fix that some of my users were able to successfully apply is to move their music files to the internal storage (reboot and wait for the media scan to finish to be sure that MediaStore is up-to-date).
in my further research for the answer, I came across this;
All about the media database (Mediastore) with android 11
Create playlist with uri "MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI", and the date row in external.db for the playlist is:
_id
_display_name
volume_name
308
New playlist.m3u
external_primary
The playlist's volume name is "external_primary".
2.
Music file is under flash card
Music file's id in external.db is 278
The volume name of flash card is "1EDD-DDE0"
When add this music file to playlist, got below exception:
Exception message: java.lang.SecurityException: has no access to content://media/external_primary/audio/media/278
If I create playlist with uri MediaStore.Audio.Playlists.getContentUri("1edd-dde0"), then music can be successfully added to the playlist.
It seems that the reason is the mismatch of volume name between playlist and the music file to be added. Only when playlist's volume name is same to music file's, inserting operation can be complete.
Update for android 11.
Worth noting that the media database has moved from
/data/data/com.android.providers.media
to
/data/data/com.google.android.providers.media.module
also the structures have changes significantly
and
I came across the same issue. As the MediaProvider changes to Google's MediaProvider, the Scoped Storage feature is activated. When you try to modify a playlist file, but it's not created by your app (or it did be created by your app, but after OTA to new Android version, which changes to use Google's MediaProvider, it scans your playlist file and put a record to its database, but leaves the owner_package_name colume empty, it's a new colume, the old MediaProvider database has no owner_package_name colume, so no one could tell this playlist file was created by you), you will get a SecurityException says you have no access to this file.
You can check if the playlist file was owned by your app before doing the 'insert' operation:
Uri uri = MediaStore.Audio.Playlists.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
String[] projection = new String[] {
MediaStore.Audio.Playlists._ID,
MediaStore.Audio.Playlists.NAME,
MediaStore.Audio.Playlists.OWNER_PACKAGE_NAME,
};
String where = MediaStore.Audio.Playlists._ID + "=?";
String[] args = new String[] {playlistFileId};
Cursor cursor = resolver.query(uri, projection, where, args, null);
if (cursor != null) {
cursor.moveToFirst();
if (!cursor.isAfterLast()) {
String ownerPkg = cursor.getString(
cursor.getColumnIndex(MediaStore.Audio.Playlists.OWNER_PACKAGE_NAME));
// print ownerPkg here
}
}
If the owner package name of this playlist file is empty or other app's package name, that you probably have no access to write this playlist file due to the scoped storage feature limit.
According to this document, we can consider using MediaStore.createWriteRequest() method to prompt user to grant write permission to playlist file for our own app, but this request only available to certain kind of files, like images, audios, videos etc, but not for some other kinds like playlist files which ends in .m3u suffix.
Also, according to this, when you try to operate some image or audio files that's not created by your app in public storage, you will get a RecoverableSecurityException and you can use this exception to prompt user to get user consent to modify the file, but for playlist kind files, you will just get SecurityException instead of RecoverableSecurityException.
So the result is, you may never be able to access to that playlist file again, you can not modify it, and you can not delete it too. My solution is just create a new playlist file, so it's owned by my app, now I finally have full access to it. You may need to migrate your old playlist data to the new one.
AND FINALLY I FIND THIS
MediaStore.Audio.Playlists
This class was deprecated in API level 31.
Android playlists are now deprecated. We (Google) will keep the current functionality for compatibility resons, but we will no longer take feature request. We do not advise adding new usages of Android Playlists. M3U files can be used as an alternative.
In conclusion, no longer a relevant post
I have implemented the SAF so do not use scopedStorage and have access once the user accepts.
The fact that I can insert new playlist entries clearly shows access to MediaStore, I can also delete these. However trying to add tracks to these playlists does not work for api29. Inserting/deleting a new playlist does not involve any files located on internal or external sdcards as it is simply adding values.
the permissions for both internal and external sdcard:
2020-07-12 14:39:04.435 11858-11858/com.flyingdutchman.newplaylistmanager E/onCreate:: uriPermission: UriPermission {uri=content://com.android.externalstorage.documents/tree/17F5-240A%3A, modeFlags=3, persistedTime=1594551961263}
2020-07-12 14:39:04.435 11858-11858/com.flyingdutchman.newplaylistmanager E/onCreate:: uriPermission: UriPermission {uri=content://com.android.externalstorage.documents/tree/primary%3A, modeFlags=3, persistedTime=1594551926876}
The question now becomes
How do I ensure saf permissions are recognised by the resolver.insert method when inserting/modify tracks into the Media database
Update May 2020
Stepping through the resolver code with debug F7
Scenario 1 results in permission error (incorrect MediaStore.VOLUME_EXTERNAL).
playlist_uri = MediaStore.Audio.Playlists.getContentUri(MediaStore.VOLUME_EXTERNAL);
playlist_members_uri = MediaStore.Audio.Playlists.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
.buildUpon()
.appendEncodedPath(Long.toString(playlist_id))
.appendEncodedPath("members")
.build();
acquireProvider(mContext, auth); = media
Uri createdRow = provider.insert(mPackageName, mAttributionTag, url, values, extras); = null
mPackageName=com.flyingdutchman.newplaylistmanager
mAttributionTag=null
values[0] = 206
values[1]=69
values[2]=1
extras=null
DatabaseUtils.java
public static final void readExceptionFromParcel(Parcel reply) {
int code = reply.readExceptionCode();
if (code == 0) return;
String msg = reply.readString();
DatabaseUtils.readExceptionFromParcel(reply, msg, code);
}
msg = com.flyingdutchman.newplaylistmanager has no access to content://media/external_primary/audio/playlists/206
Scenario 2 results in NO permission error BUT no tracks added to audio_playlists_map table.
playlist_uri = MediaStore.Audio.Playlists.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
url=content://media/external_primary/audio/playlists/206/members
Does anyone have a recommendation for the best way to handle URI results from the ACTION_GET_CONTENT intent?
I am finding that applications that handle the intent provide different data back in return. When selecting files from the Download directory in three different file pickers I get significantly different results.
The KitKat download gallery:
content://com.android.providers.downloads.documents/document/1438
Genymotion (CyanogenMod) Browser:
file:///storage/emulated/0/Download/334SIGCO-PHRH-FEB14.xls
Android
File Manager:
content://com.smartwho.SmartFileManager/mimetype//storage/emulated/0/Download/334
PhrPrint.xls
I'm concerned that when I release my app I will get responses back from the user that I didn't anticipate in code and it will crash. Given that the three tools I've tested have all provided different answers, I'm concerned for what will happen in the wild.
Ultimately I will need to read the contents of the selected file into an InputStream and read the data (.xls via jxl) into my data structure. But first I need to check the data to get the file name and verify the data type so I can provide feedback to the user that they have a valid file type.
I can use the ContentResolver to do that with the KitKat Download Gallery, but the code fails for both of the other options. Code follows:
public void setFileUri(Uri fileUri) {
ContentResolver resolver = mFragment.getActivity().getContentResolver();
//Check if the file is of the right data type
if(resolver.getType(fileUri).equals(REQUIRED_FILE_TYPE)) {
mFileUri = fileUri;
mComplete = true;
}
Cursor cursor = resolver.query(fileUri, null, null, null, null);
cursor.moveToFirst();
((FileSelectFragment)mFragment).setFileNameView(cursor.getString(
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)),
mComplete);
}
Thank you in advance.
I open a picture in album and get the Uri. Then I convert the Uri to a file path. In the log it shows as something like mnt/storage/emulated/0/xxx.jpg. I covert Uri to file path as the way like:
Cursor cursor = GlobalObjectManager.getInstance().getContext().getContentResolver()
.query(filePathUri, null, null, null, null);
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
fileName = cursor.getString(column_index);
The problem is that when I open the file with function it catches a FileNotFoundException.
String path = "mnt/storage/emulated/0/xxx.jpg";
FileInputStream in = new FileInputStream(path);
the code works well on other devices with Android 2.3-4.1.
So far as I know is that my Nexus 4 runs Android 4.2 and mnt/storage/emulated/0/ works for multi-user.
In my app I must use FileInputStream() function to read byte data of the beginning of the file.
Could anyone tell me how to fix the bug? Thanks!
ok i fix it. I made a big mistake! I add mnt/ in front of storage/ needlessly, and it takes the bug.
I believe you are seeing /storage/emulated/0. We're seeing this problem too, it seems to be related to the new /storage handling for multiple SD cards, I think it was introduced in Android 4.1 but maybe later. If you look, you'll see that /storage/emulated/0 does not exist on the filesystem, not even as a symlink. Who knows what the system is using that path or what tricks are going on there.
The workaround is to do:
fileName = new File(cursor.getString(column_index)).getCanonicalPath();
I think you should not have the "mnt" ,this is to say,you can code as this:
String path = "mnt/storage/emulated/0/xxx.jpg";
FileInputStream in = new FileInputStream(path);
Since Android 4.2 if a user downloads some file in the browser the DownloadManager is used. If the user clicks the 'download complete' notification an Intent is and was always launched. Before Android 4.2 the intent used to have the downloaded file's path in the content, such that:
intent.getData()
would return a String such as file:///storage/emulated/0/Download/some_file.ext. However, since Android 4.2 the download manager broadcasts and intent with a content scheme, such as content://downloads/all_downloads/2334.
How do I retrieve the local file path for a downloaded file?
I've tried the following:
public static String getRealPathFromURI(Uri contentUri, Activity activity) {
DownloadManager downloadManager = (DownloadManager) activity.getSystemService(Activity.DOWNLOAD_SERVICE);
String[] contentParts = contentUri.getEncodedPath().split("/");
Cursor q = downloadManager.query(new DownloadManager.Query().setFilterById(Integer.parseInt(contentParts[contentParts.length - 1])));
if (q == null) {
// Download no longer exists
return null;
}
q.moveToFirst();
return q.getString(q.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME));
}
But the cursor never returns any rows (so q.getCount() == 0 and therefor the last return statement throws an exception). Also, the hack by parsing the download file id from the Uri seems odd.
UPDATE: I have also tried:
input = getActivity().getContentResolver().openInputStream(contentUri);
but this returns an error stating
Permission Denial: reading com.android.providers.downloads.DownloadProvider uri content://downloads/all_downloads/2334 from pid=30950, uid=10064 requires android.permission.ACCESS_ALL_DOWNLOADS, or grantUriPermission()
Clearly I can't access the downloads (as my app did not initiate them - the browser did) through the ContentProvider.
Here's what worked. First, you can't (and shouldn't want to) get the file path as botteaap correctly pointed out. (Credits to him for setting me on the right path.) Instead you get a temporary permission automatically to access the file content, using:
InputStream input = getContentResolver().openInputStream(intent.getData());
You can read this InputStream like any other in Java. It seems there is no way to get the file name. If you need a file, first write the input stream to a (temporary) file.
The SecurityException is throw when your temporary access was revoked. This happend for me on some files that I tried to read incorrectly before and specifically when the Intent was picked up in my Acticity's onNewIntent.
Getting it through the content resolver is the right thing. Not every content url is going to be a file. For example, the Gallery app will give you uri's that translate to a network call or a local file depending on the source.
Even if you'd get to the real file path, you'll probably unable to read it, due to file permissions, although you can be lucky it it's on external storage. Have you tried adding android.permission.ACCESS_ALL_DOWNLOADS to your app like the exception suggests? That won't work, since the permission is at signature level :(
I just want to add to the answer from #erickok as it took me several hours to figure this out. As stated by #jcesarmobile, you are only guaranteed to be able to get the name and size of the file, not the full path.
You can get the name and size as follows, and then save to a temp file:
String filename = null;
Long filesize = null;
Cursor cursor = null;
try {
cursor = this.getContentResolver().query(intent.getData(), new String[] {
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, null, null, null );
if (cursor != null && cursor.moveToFirst()) {
filename = cursor.getString(0);
filesize = cursor.getLong(1);
}
} finally {
if (cursor != null)
cursor.close();
}
I'm querying all images on the Android device as such:
string[] columns = { MediaStore.Images.Media.InterfaceConsts.Data,
MediaStore.Images.Media.InterfaceConsts.Id };
string orderBy = MediaStore.Images.Media.InterfaceConsts.Id;
var imagecursor = ManagedQuery(MediaStore.Images.Media.ExternalContentUri, columns, null, null, orderBy);
for (int i = 0; i < this.Count; i++) {
imagecursor.MoveToPosition(i);
Paths[i]= imagecursor.GetString(dataColumnIndex);
Console.WriteLine(Paths[i]);
Console.WriteLine(System.IO.File.Exists(Paths[i]));
}
The problem is that the output shows that some files don't exist. Here's a sample output:
/storage/sdcard0/Download/On-Yom-Kippur-Jews-choose-different-shoes-VSETQJ6-x-large.jpg
False
/storage/sdcard0/Download/397277_10151250943161341_876027377_n.jpg
False
/storage/sdcard0/Download/Roxy_Cottontail_&_Melo-X_Present..._Some_Bunny_Love's_You.jpg
False
/storage/sdcard0/Download/album-The-Rolling-Stones-Some-Girls.jpg
True
/storage/sdcard0/Download/some-people-ust-dont-appreciate-fashion[1].jpg
True
/storage/sdcard0/Download/express.gif
True
...
/storage/sdcard0/Download/some-joys-are-expressed-better-in-silence.JPG
False
How is this possible? I downloaded these images myself from the internet! They should exist in disk.
You appear to be using a LoaderMananger/ManagedQuery to query the Media Content Provider in Android. A Content Provider is just a way to access a particular SQLite database from different apps. If you use the Android provided Media Content Provider you'll have to update it 'manually' by using MediaScannerConnection to add in the new files that you've placed, as the 'service' may or may not update internally while your app is running.
Here are some related SO questions:
Scan Android SD card for new files
and Trigger mediascanner on specific path (folder), how to? but I don't recommend the answer of globally scanning your SD card.
If this is a Samsung device, all those paths may be incorrect !
// Put this in your code and then log the 'external' string to see
String external = Environment.getExternalStorageDirectory();
if you get say: /storage/sdcard0/
Then Samsung themselves say you have to do this lame append (I claim they broke the API :)
external = external + "/external_sd/";
I am not certain this applies to downloads or not, but I suspect it affects ALL similar sub-paths.
http://developer.samsung.com/forum/board/thread/view.do?boardName=GeneralB&messageId=162934&messageNumber=1381&startId=zzzzz~&searchType=TITLE&searchText=sdcard
[EDIT:] Caveat Emptor ! Even this does not work on some Samsung devices. ARRRRGGGGGG