External Storage Permission Issue with MediaProvider / Ring Tones
Asked Answered
F

3

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.

Fatality answered 17/1, 2014 at 21:42 Comment(1)
can you post your code?Anticipate
O
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));
Occupation answered 24/8, 2014 at 22:8 Comment(2)
I haven't tested it, but that looks like it will work.Fatality
I've tested it in 4.4.4 and checked against the sources of 4.0.3, so it should work with API 15 and upwardsOccupation
I
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;
}
Impromptu answered 21/1, 2014 at 11:45 Comment(10)
I think the OP specifically wants to know if this can be done by using RingtoneManagerProximity
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.Impromptu
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!"Proximity
Then perform the filtering of the results from RingtoneManager before returning them to the UI.Impromptu
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.Fatality
Can you confirm how you are creating the cursor object for your call to setSingleChoiceItems()?Impromptu
I am getting the cursor with ringtoneManager.getCursor()Fatality
Edited my answer to (hopefully) cover what you're after.Impromptu
I havent tested it yet, but it appears that if anything will do the trick itll be this. thank you.Fatality
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.Fatality
F
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();                                                                                                    
}  
Fatality answered 2/2, 2014 at 21:13 Comment(2)
but this line still causes a security exception: Ringtone ringtone = RingtoneManager.getRingtone(context, uri); Vern
@behelit, That seems strange to me. the docs for getRingtone 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.Fatality

© 2022 - 2024 — McMap. All rights reserved.