You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.23 14:54:02