5

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.

Reed
  • 14,703
  • 8
  • 66
  • 110

3 Answers3

3

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));
atomicode
  • 581
  • 4
  • 11
2

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;
}
Alex Tennant
  • 356
  • 3
  • 9
  • I think the OP specifically wants to know if this can be done by using `RingtoneManager` – Magnus Jan 21 '14 at 12:39
  • 1
    `RingtoneManager` just lets you query the various different media providers, and from the get URIs or the `Ringtone` objects themselves. I'm suggesting he makes use of `RingtoneManager` to perform the initial query, and then filters the results from that before requesting the `Ringtone` objects so that he's not making requests for `Ringtone` objects on the external storage. Edited my original answer to make this clearer. – Alex Tennant Jan 21 '14 at 12:43
  • Yes of course, but the whole purpose (and cleaner solution) would be if you could filter *before* you pick something. It's quite user unfriendly to let them choose a ringtone and then toast a message saying "sorry that was on the sdcard, please choose a new one!" – Magnus Jan 21 '14 at 12:49
  • Then perform the filtering of the results from `RingtoneManager` before returning them to the UI. – Alex Tennant Jan 21 '14 at 15:19
  • 1
    I am wanting to know if I can use RingtoneManager to exclude tones on the external storage so those are not in the list returned by `setSingleChoiceItems(cursor, position, c.getColumnName(RingtoneManager.TITLE_COLUMN_INDEX)` along with a brief example or a useful link that would help me with this. – Reed Jan 22 '14 at 00:22
  • Can you confirm how you are creating the `cursor` object for your call to `setSingleChoiceItems()`? – Alex Tennant Jan 22 '14 at 13:41
  • I am getting the cursor with `ringtoneManager.getCursor()` – Reed Jan 22 '14 at 22:36
  • 1
    Edited my answer to (hopefully) cover what you're after. – Alex Tennant Jan 23 '14 at 10:34
  • 1
    I havent tested it yet, but it appears that if anything will do the trick itll be this. thank you. – Reed Jan 24 '14 at 05:01
  • I just looked over my stacktraces again, and the exception is actually being thrown at `at android.media.RingtoneManager.getMediaRingtones(RingtoneManager.java:510) at android.media.RingtoneManager.getCursor(RingtoneManager.java:372)` which means I can't analyze the cursor to exclude items on the external storage. – Reed Feb 01 '14 at 22:55
2

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();                                                                                                    
}  
Reed
  • 14,703
  • 8
  • 66
  • 110
  • but this line still causes a security exception: `Ringtone ringtone = RingtoneManager.getRingtone(context, uri); ` – behelit Nov 14 '18 at 02:10
  • @behelit, That seems strange to me. the docs for [`getRingtone`](https://developer.android.com/reference/android/media/RingtoneManager#getRingtone(android.content.Context,%20android.net.Uri)) do not mention a `SecurityException`. It's been so long since I did Android, that I don't really know what I'm talking about though. – Reed Nov 23 '18 at 19:35