READ_EXTERNAL_STORAGE 在 MediaPlayer 中打开内容需要权限://media/external/[...]?

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

我正在我的应用程序中播放闹钟声。在大多数设备上,这都能完美运行。一些用户描述说没有声音在播放。这似乎主要影响三星和华为设备。这是失败的最小示例:

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);
        }

对于受影响的用户,抛出以下异常:

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

这似乎只有当 alarmURI 看起来像这样时才会发生:content://media/external/audio/media/11,即它驻留在外部存储中。当 alarmURI 看起来是这个 content://media/internal/audio/media/40.

时,我从未见过它发生

如果我使用的 Uri 包含 /external/ 部分,我是否需要 READ_EXTERNAL_STORAGE 权限才能播放声音?还是我错过了其他东西?通常,如果权限是问题,我希望抛出 SecurityException。

/e:我可以在我的设备上重现该错误。这是更详细的错误报告:

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()

Normally, I would expect a SecurityException

您没有得到 SecurityException,因为您的代码在达到 Linux 文件系统(OS 级别)权限时失败,而不是 Android 框架权限。对于从 API 接收到的 RuntimeException 没有严格的保证(例如 SecurityException),这就是它们首先被称为 "unchecked Exceptions" 的原因。

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

切勿使用外部提供的 Uri 的内容在您的代码中做出任何决定。将它们视为不透明字符串。

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…

您发布用于故障排除的 Uris 是正确的,但了解 Uri 的来源也很有帮助。 Android 的现代版本使用 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 到 Intent(或调用 Context#grantUriPermission 以获得相同的结果),以及您是否在收到带有 Uri 的 Intent 后调用了 Context#takePersistableUriPermission

是的,您可能需要 READ_EXTERNAL_STORAGE 许可。 Android 使用 FUSE 或类似供应商特定的 API 根据调用应用程序的权限调整外部存储的可见 OS 级权限:当您的应用程序具有权限时,外部文件看起来可读它 (File#isReadable returns true),当它不存在时,它看起来无法访问外部文件 (File#isReadable returns false).当然,这应该只与文件相关,但是如果调用者给你一个 file:// Uri 怎么办?此外,Uri 后面的 ContentProvider 可能会在向您提供文件描述符之前检查您是否拥有 READ_EXTERNAL_STORAGE 权限(这很愚蠢,但肯定是可能的)。

此外,最好自己打开 Uri 并向播放器提供 stream/file 描述符而不是 Uri。大多数 Linux 安全检查* 在您打开文件描述符时发生。为了实现可预测的行为,您应该在自己的代码中打开 Uri 并尽可能多地进行错误处理,包括从远程 ContentProvider 捕获 RuntimeException。

为什么会出现这个问题?可能有多种原因,但可能是以下原因之一:

  1. 有问题的设备上的 MediaStorage ContentProvider 存在问题(制造商可以并且经常使用自定义有问题的版本替换系统 ContentProvider)
  2. 有问题的设备上的 MediaPlayer 实现有问题
  3. 错误设备上的 SELinux 权限存在缺陷(过度限制对文件的访问)
  4. 您的代码接收 Uris,将它们存储在某处,但不调用 takePersistableUriPermission,因此您的访问权限在应用重启后失效。

如果没有异常堆栈跟踪,很难诊断出确切的问题。无论哪种方式,如果上述建议没有帮助,除了使用不同的 API 播放声音文件 and/or 强制用户在不使用 Android MediaProvider 的情况下选择警报声音之外,您几乎无能为力。


* 除了一些 SELinux 检查,但你真的不能对那些