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+如果要分享应用专属目录的文件,不需要额外权限 -->
关键优化点总结
- 权限时机提前:保存文件前就检查权限,而不是等到分享时才申请。
- 分区存储适配:API29+用应用专属外部存储,避免权限问题。
- FileProvider全覆盖:API24+必须用FileProvider,不能再用
file://Uri。 - 流的安全关闭:把流的关闭放在finally块里,防止内存泄漏。
按照上面的修改,应该能解决从API16到最新Android版本的音频保存和分享问题,赶紧测试上线吧!
内容的提问来源于stack exchange,提问作者HavanaSun




