Play Store更新后MediaPlayer.setDataSource抛IOException的权限问题
本地音频应用URI访问报错问题
应用功能与异常表现
- 应用核心能力:支持播放用户设备本地存储的音频,可将不同音频分配到自定义触控垫,用户可自由设置每个触控垫对应的音频资源,点击触控垫即可触发对应音效播放。
- 异常触发场景:应用向Google Play生产渠道推送更新版本后,已经加载过音频、且已授予相关权限的存量用户,在调用
setDataSource()方法时会抛出IOException,错误信息为:
setDataSource failed.: status=0x80000000
- 复现特征:在自有设备上测试时发现,将同一份音频重新加载到其他触控垫后,原本绑定该音频的触控垫会突然恢复正常工作,初步判断问题根源与URI权限相关。
现有实现逻辑
- 权限与音频选取流程:
- 先申请
READ_EXTERNAL_STORAGE权限,用户完成授权后拉起系统文件选择器选取音频文件 - 拉起文件选择器时附加
FLAG_GRANT_READ_URI_PERMISSION、FLAG_GRANT_PERSISTABLE_URI_PERMISSION标记,选取文件后通过ContentResolver.takePersistableUriPermission()保存URI的持久化访问权限,保证设备重启后仍可正常读取音频
- 先申请
- 报错触发位置:异常发生在
playButtonIndexSound()方法执行mediaPlayer.setDataSource(this, currentUri)环节,核心实现代码如下:
private void requestReadExternalStoragePermission() { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 11); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == 11 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { findNewMedia(); } else { Toast.makeText(this, "Permission to access this device's external storage has been denied.", Toast.LENGTH_LONG).show(); } } private void findNewMedia() { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { requestReadExternalStoragePermission(); } else { permissionGrantedGetNewMedia(); } } private void permissionGrantedGetNewMedia() { try { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); // Persist the permission, even on phone restart intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); intent.setType("audio/*"); startActivityForResult(intent, OPEN_REQUEST_CODE); } catch (ActivityNotFoundException e) { Toast.makeText(this, "This device has not provided a way to access it's external storage. You are unable to load sounds.", Toast.LENGTH_LONG).show(); } catch (Exception e) { FirebaseCrashlytics.getInstance().recordException(e); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { super.onActivityResult(requestCode, resultCode, resultData); currentUri = null; if (resultCode == Activity.RESULT_OK) { if (requestCode == OPEN_REQUEST_CODE) { if (resultData != null) { Log.d(TAG, "onActivityResult: resultData.getData(): " + resultData.getData()); currentUri = resultData.getData(); assert currentUri != null; // Persist the permission, even on phone restart int takeFlags = resultData.getFlags(); takeFlags &= (Intent.FLAG_GRANT_READ_URI_PERMISSION); ContentResolver resolver = this.getContentResolver(); resolver.takePersistableUriPermission(currentUri, takeFlags); } } } } private void playButtonIndexSound(final MediaPlayer mediaPlayer, int btnNumber) { // currentSoundObject is a global variable set in another method currentUri = Uri.parse(currentSoundObject.getUri()); if (!mediaPlayer.isPlaying()) { try { mediaPlayer.reset(); } catch (Exception e) { FirebaseCrashlytics.getInstance().recordException(e); Toast.makeText(this, "Error resetting MediaPlayer", Toast.LENGTH_SHORT).show(); } try { mediaPlayer.setDataSource(this, currentUri); } catch (Exception e) { FirebaseCrashlytics.getInstance().recordException(e); Toast.makeText(this, "Error setting data source MediaPlayer", Toast.LENGTH_SHORT).show(); } if (loopSounds) { try { mediaPlayer.setLooping(true); } catch (Exception e) { FirebaseCrashlytics.getInstance().recordException(e); Toast.makeText(this, "Error setting loop function", Toast.LENGTH_SHORT).show(); } } try { mediaPlayer.prepareAsync(); } catch (Exception e) { FirebaseCrashlytics.getInstance().recordException(e); Toast.makeText(this, "Error preparing MediaPlayer", Toast.LENGTH_SHORT).show(); } } else { try { mediaPlayer.reset(); } catch (Exception e) { FirebaseCrashlytics.getInstance().recordException(e); Toast.makeText(this, "Error resetting MediaPlayer", Toast.LENGTH_SHORT).show(); } } mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { try { mp.start(); } catch (Exception e) { FirebaseCrashlytics.getInstance().recordException(e); Toast.makeText(this, "Error starting MediaPlayer", Toast.LENGTH_SHORT).show(); } } }); }
排查方向
- 核查升级场景下的URI权限留存状态:应用版本升级后,部分系统会清除旧版本留存的URI持久化访问权限,建议在每次通过URI读取音频前,主动校验当前应用对该URI是否仍具备合法的读权限,不能默认已申请的持久权限永久有效。
- 排查全局URI变量的覆盖问题:现有代码使用全局变量
currentUri保存待访问的音频URI,多触控垫场景下,若用户为其他触控垫重新选择音频,会直接覆盖全局变量的取值,导致原有触控垫触发播放时拿到无效URI。这一问题和复现特征完全吻合:将同一份音频重新加载到其他触控垫时,相当于重新为该URI申请了一次访问权限,原绑定位的访问限制被解除,因此恢复正常工作。 - 修正持久权限申请的flags计算逻辑:当前代码通过按位与操作从系统返回的Intent中提取读权限标记,若定制系统返回的Intent未包含对应标记,最终传递给
takePersistableUriPermission()的权限值会为0,无法获得合法的访问资格,可直接固定传入Intent.FLAG_GRANT_READ_URI_PERMISSION作为权限申请参数,避免位运算结果异常。
内容的提问来源于stack exchange,提问作者Tom Roe
相关产品推荐
相关产品推荐

