Some of my users have reported to Google Play the following error when trying to select a ringtone in my app. (there's more to it, but it's not relevant)
java.lang.SecurityException: Permission Denial:
reading com.android.providers.media.MediaProvider
uri content://media/external/audio/media
from pid=5738, uid=10122 requires android.permission.READ_EXTERNAL_STORAGE
I assume this issue is happening due to certain tones that are on external storage. I don't want to include the READ_EXTERNAL_STORAGE
permission in my app unless I absolutely have to.
Is there a way to circumvent the issue and just exclude any tones that may be on the external storage?
Note: I'm getting ringtones with RingtoneManager
and convert them between String
and Uri
. No other code is touching the user's media.
Also, I do not have a line number since the stacktrace is from obfuscated code and re-mapping the stack trace did not provide line number.
Just had the same problem and came up with the following solution:
private Cursor createCursor()
{
Uri uri = MediaStore.Audio.Media.INTERNAL_CONTENT_URI;
String[] columns = new String[]
{
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.TITLE_KEY
};
String filter = createBooleanFilter(MediaStore.Audio.AudioColumns.IS_ALARM);
String order = MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
return getContext().getContentResolver().query(uri, columns, filter, null, order);
}
private String createBooleanFilter(String... columns)
{
if(columns.length > 0)
{
StringBuilder sb = new StringBuilder();
sb.append("(");
for(int i = columns.length - 1; i > 0; i--)
{
sb.append(columns[i]).append("=1 or ");
}
sb.append(columns[0]);
sb.append(")");
return sb.toString();
}
return null;
}
To get the Uri of a ringtone you need to combine the INTERNAL_CONTENT_URI
with the _ID
column value, you can do this by using ContentUris
class:
Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.INTERNAL_CONTENT_URI, cursor.getLong(0));
You can find the external storage directory without needing WRITE_EXTERNAL_STORAGE
or READ_EXTERNAL_STORAGE
by using Environment.getExternalStorageDirectory()
.
You can then compare the paths for the URIs provided by RingtoneManager
to this path to see if they are on the external storage or not and if so add those items to a List
.
Then, rather than passing the raw Cursor
to the UI you can use that List
with a ListAdapter
instead.
For example (untested, you may need to change the method of comparing paths):
class RingtoneDetails
{
public String ID;
public String Title;
public Uri Uri;
public RingtoneDetails(String id, String title, Uri uri)
{
ID = id;
Title = title;
Uri = uri;
}
}
private List<RingtoneDetails> getNonExternalRingtones(RingtoneManager manager)
{
List<RingtoneDetails> ringtones = new List<RingtoneDetails>();
Cursor cursor = manager.getCursor();
String extDir = Environment.getExternalStorageDirectory().getAbsolutePath();
while (cursor.moveToNext())
{
String id = cursor.getString(cursor.getColumnIndex(RingtoneManager.ID_COLUMN_INDEX));
String title = cursor.getString(cursor.getColumnIndex(RingtoneManager.TITLE_COLUMN_INDEX));
Uri uri= cursor.getString(cursor.getColumnIndex(RingtoneManager.URI_COLUMN_INDEX));
if(!uri.getPath().contains(extDir))
{
ringtones.add(new Ringtone(id, title, uri));
}
}
return ringtones;
}
Previously, I was using RingtoneManager
to get a list and display that in a dialog for a user to select. It was throwing the SecurityException
on ringtoneManager.getCursor();
I did not want to add the external storage permission, so I switched to doing:
final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, "Select Ringtone");
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE,RingtoneManager.TYPE_ALL);
startActivityForResult( intent, RINGTONE_RESULT);
And then in onActivityResult
if (requestCode == RINGTONE_RESULT&&resultCode == RESULT_OK&&data!=null) {
try {
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
if (uri==null){
setSilent(); //UI stuff in this method
} else {
Ringtone ringtone = RingtoneManager.getRingtone(context, uri);
String name = ringtone.getTitle(context);
changeTone.setText(name); //changeTone is a button
}
} catch (SecurityException e){
setSilent();
Toast.makeText(context, "Error. Tone on user storage. Select a different ringtone.", Toast.LENGTH_LONG).show();
} catch (Exception e){
setSilent();
Toast.makeText(context, "Unknown error. Select a different ringtone.", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(context, "Ringtone not selected. Tone set to silent.", Toast.LENGTH_SHORT).show();
setSilent();
}