You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

AndroidX迁移后音频分享权限异常及多版本兼容修复咨询

兄弟,看你这情况急得不行——刚迁AndroidX就炸了音频分享,还全版本崩,差评找上门确实头疼。我帮你拆解下问题,核心是权限时机错了+跨版本存储逻辑没跟上+FileProvider配置不到位,和AndroidX其实关系不大,主要是系统版本的权限/存储规则变了。下面一步步给你改,保证从API16到最新版都能用:

1. 先搞懂核心问题出在哪

  • 你之前的代码保存文件时完全没检查权限,只在分享时才申请,导致Android 6(API23)+的设备还没等分享就已经保存失败了,这就是Android9及以下也炸的根本原因!
  • Android 10(API29)开始启用分区存储,Environment.getExternalStorageDirectory()被废弃,直接写公共目录会触发权限拒绝。
  • Android 7(API24)开始禁止直接用file://Uri分享文件,必须用FileProvider,你之前只判断到API22,这也会导致高版本崩溃。

2. 第一步:配置AndroidX版FileProvider

因为你迁了AndroidX,必须用AndroidX的FileProvider,先在AndroidManifest.xml里加配置:

<application>
    ...
    <!-- FileProvider配置 -->
    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
</application>

然后在res/xml目录下新建file_paths.xml(没有xml目录就自己建):

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 对应应用专属外部存储目录,无需WRITE权限 -->
    <external-files-path
        name="app_sounds"
        path="appfolder/" />
    <!-- 兼容API<29的公共存储路径(可选,如果你想保留旧路径) -->
    <!--<external-path name="external_files" path="appfolder/" />-->
</paths>

3. 修改保存音频的代码(全版本兼容)

重点:保存前先检查权限,API29+用应用专属外部存储(无需权限),API<29才需要WRITE权限:

// 抽成单独方法,方便复用
private void saveAndShareAudio(View view, SoundObject soundObject) {
    final String fileName = soundObject.getItemName().replaceAll("[^a-zA-Z0-9._-]", "_") + ".mp3";
    File file;

    // 分版本处理存储路径
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // API29+:用应用专属外部存储,无需WRITE权限,卸载会自动删除
        File externalFilesDir = view.getContext().getExternalFilesDir(Environment.DIRECTORY_MUSIC);
        file = new File(externalFilesDir, "appfolder/" + fileName);
    } else {
        // API<29:用公共存储,需要WRITE权限
        File storage = Environment.getExternalStorageDirectory();
        file = new File(storage.getAbsolutePath() + "/appfolder/" + fileName);
    }

    // 创建父目录
    if (!file.getParentFile().exists()) {
        file.getParentFile().mkdirs();
    }

    // 权限检查:仅API23+需要动态申请
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (ContextCompat.checkSelfPermission(view.getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions((Activity) view.getContext(), 
                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1001);
            return; // 等权限申请结果回来再执行
        }
    }

    // 开始保存文件
    InputStream in = null;
    OutputStream out = null;
    try{
        Log.i(LOG_TAG, "Saving sound " + soundObject.getItemName());
        in = view.getContext().getResources().openRawResource(soundObject.getItemID());
        out = new FileOutputStream(file);
        byte[] buffer = new byte[1024];
        int len;
        while ((len = in.read(buffer, 0, buffer.length)) != -1){
            out.write(buffer, 0 , len);
        }
        // 保存成功后直接触发分享
        shareAudio(view.getContext(), file);
    } catch (IOException e){
        Log.e(LOG_TAG, "Failed to save file: " + e.getMessage());
        Toast.makeText(view.getContext(), "Save failed", Toast.LENGTH_SHORT).show();
    } finally {
        // 必须在finally关闭流,防止内存泄漏
        try {
            if (in != null) in.close();
            if (out != null) out.close();
        } catch (IOException e) {
            Log.e(LOG_TAG, "Failed to close streams: " + e.getMessage());
        }
    }
}

4. 修改分享音频的代码(全版本兼容)

抽成单独方法,重点处理API24+的FileProvider逻辑:

private void shareAudio(Context context, File file) {
    if (!file.exists()) {
        Log.e(LOG_TAG, "File not found: " + file.getAbsolutePath());
        Toast.makeText(context, "Audio file missing", Toast.LENGTH_SHORT).show();
        return;
    }

    Intent intent = new Intent(Intent.ACTION_SEND);
    Uri contentUri;

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        // API24+必须用FileProvider,不能直接用file://Uri
        String AUTHORITY = context.getPackageName() + ".fileprovider";
        contentUri = FileProvider.getUriForFile(context, AUTHORITY, file);
        // 给接收方授予临时读取权限
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    } else {
        // API<24可以直接用file://Uri
        contentUri = Uri.fromFile(file);
    }

    intent.putExtra(Intent.EXTRA_STREAM, contentUri);
    intent.setType("audio/mp3");

    // 检查是否有应用能处理这个Intent,避免崩溃
    if (intent.resolveActivity(context.getPackageManager()) != null) {
        context.startActivity(Intent.createChooser(intent, "Share sound via..."));
    } else {
        Toast.makeText(context, "No apps available to share", Toast.LENGTH_SHORT).show();
    }
}

5. 处理权限申请结果

在你的Activity里重写权限回调:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == 1001) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 用户同意权限,重新执行保存逻辑
            // 这里需要把之前的SoundObject参数传过来,比如用成员变量保存
            saveAndShareAudio(mCurrentView, mCurrentSoundObject);
        } else {
            Toast.makeText(this, "Permission denied, cannot save audio", Toast.LENGTH_SHORT).show();
        }
    }
}

6. 最后补全Manifest权限

AndroidManifest.xml里加权限,注意给API29+的设备跳过:

<!-- API<29需要的WRITE权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<!-- API30+如果要分享应用专属目录的文件,不需要额外权限 -->

关键优化点总结

  1. 权限时机提前:保存文件前就检查权限,而不是等到分享时才申请。
  2. 分区存储适配:API29+用应用专属外部存储,避免权限问题。
  3. FileProvider全覆盖:API24+必须用FileProvider,不能再用file://Uri。
  4. 流的安全关闭:把流的关闭放在finally块里,防止内存泄漏。

按照上面的修改,应该能解决从API16到最新Android版本的音频保存和分享问题,赶紧测试上线吧!

内容的提问来源于stack exchange,提问作者HavanaSun

火山引擎 最新活动