Search code examples
androidpermissionsandroid-mediaplayerandroid-contentproviderandroid-external-storage

READ_EXTERNAL_STORAGE permission needed to open content://media/external/[...] in MediaPlayer?


I'm playing an alarm sound in my App. On most devices this works flawlessly. Some users have described that no sound is playing. This seems to mainly affect Samsung and Huawei devices. This is a minimal example of where things fail:

alarmUri = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
try {
            if(!mediaPlayer.isPlaying()) {
                mediaPlayer.setDataSource(this, alarmUri);   
                AudioManager audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE);
                mediaPlayer.setAudioStreamType(audioManager.STREAM_ALARM);
                mediaPlayer.setLooping(true);
                mediaPlayer.prepare();
                mediaPlayer.start();
            }
        } catch(Exception e) {                
                Log.d(logtag,"Exception: " + e);
        }

For affected users, the following exception is thrown:

Exception java.io.IOException: setDataSource failed.: status=0x80000000

This only seems to happen when alarmURI looks like this: content://media/external/audio/media/11, i.e. it resides on an external store. I have never seen it occur when alarmURI looks this this content://media/internal/audio/media/40.

Do I need the READ_EXTERNAL_STORAGE permission in order to play the sound, if the Uri I use contains the /external/ part? Or am I missing something else? Normally, I would expect a SecurityException to be thrown, if the permission was the issue.

/e: I could reproduce the bug on my device. Here is a more detailed error report:

Exception java.lang.SecurityException: Permission Denial: reading com.android.providers.media.MediaProvider uri content://media/external/audio/media from pid=27158, uid=10177 requires android.permission.READ_EXTERNAL_STORAGE, or grantUriPermission()


Solution

  • Normally, I would expect a SecurityException

    You don't get SecurityException, because you code fails upon hitting Linux filesystem (OS-level) permissions, not Android framework permissions. There are no strict guarantees about RuntimeExceptions you receive from API (such as SecurityException), this is why they are called "unchecked Exceptions" in the first place.

    Do I need the READ_EXTERNAL_STORAGE permission in order to play the sound, if the Uri I use contains the /external/ part?

    Never use contents of externally-provided Uri to make any decisions in your code. Just treat them as opaque strings.

    This only seems to happen when alarmURI looks like this…, i.e. it resides on an external store. I have never seen it occur when alarmURI looks this this…

    You are correct in posting the Uris for troubleshooting, but it is also helpful to know, where the Uri comes from. Modern versions of Android use dynamic Uri permission model. That is: your level of access to Uri may differ, depending on where Uri came from and whether the caller has added FLAG_GRANT_READ_URI_PERMISSION to Intent (or called Context#grantUriPermission for the same result), and whether you have called Context#takePersistableUriPermission upon receiving the Intent with Uri.

    Yes, you might need READ_EXTERNAL_STORAGE permission. Android uses FUSE or similar vendor-specific API to adjust visible OS-level permissions of external storage depending on permission of calling application: when your app has the permission, external files look readable to it (File#isReadable returns true), when it does not, external files look inaccessible to it (File#isReadable returns false). Of course, that should only be relevant for files, but what if the caller gives you a file:// Uri? Also the ContentProvider behind the Uri may check if you have the READ_EXTERNAL_STORAGE permission before giving you the file descriptor (it's silly, but certainly possible).

    In addition, it is a good idea to open the Uri yourself and supply the stream/file descriptor to the the player instead of Uri. Most Linux security checks* happen when you open a file descriptor. In order to achieve predictable behavior, you should open the Uri in your own code and do as much error handling as possible, including catching RuntimeException from remote ContentProvider.

    Why does the issue arise? There could be multiple reasons, but it is probably one of these:

    1. The MediaStorage ContentProvider on guilty devices is simply buggy (manufacturers can and often do replace system ContentProviders with custom buggy versions)
    2. The MediaPlayer implementation on guilty devices is buggy
    3. The SELinux permissions on guilty devices are flawed (restrict the access to files too much)
    4. Your code receives Uris, stores them somewhere, but does not call takePersistableUriPermission, so your access is invalidated after app restart.

    It is hard to diagnose the exact issue without Exception stack trace. Either way, if suggestions above don't help, there is little you could do aside from using different API for playing sound files and/or forcing user to pick alarm sound without using Android MediaProvider.


    * except some SELinux checks, but you can't really do anything about those