
本文详解 android 9(pie)及以上版本(尤其是 android 11+)中因存储权限变更导致铃声无法保存的问题,提供兼容 sdk 28–34 的权限适配、文件写入与媒体库插入全流程修复方案。
本文详解 android 9(pie)及以上版本(尤其是 android 11+)中因存储权限变更导致铃声无法保存的问题,提供兼容 sdk 28–34 的权限适配、文件写入与媒体库插入全流程修复方案。
Android 自 Android 9(API 28)起逐步收紧外部存储访问权限,至 Android 11(API 30)引入分区存储(Scoped Storage),并废弃 WRITE_EXTERNAL_STORAGE 权限对公共目录的写入能力;Android 12(API 31)起更强制要求应用若需访问所有文件(如 Music/ 目录),必须申请 MANAGE_EXTERNAL_STORAGE 权限,并通过系统设置页引导用户手动授权。原代码中直接使用 Environment.getExternalStoragePublicDirectory() + WRITE_EXTERNAL_STORAGE 在新系统上必然失败,且 MediaStore.Audio.Media.getContentUriForPath() 在 Android 10+ 已被弃用,需彻底重构。
✅ 关键修复点说明
-
动态权限逻辑分层适配
- API :仍可请求 WRITE_EXTERNAL_STORAGE + READ_EXTERNAL_STORAGE(需在 AndroidManifest.xml 中声明);
- API ≥ 31(Android 12+):必须使用 MANAGE_EXTERNAL_STORAGE,并通过 Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION(优先)或 Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION(兼容 fallback)跳转设置页;
- 注意:MANAGE_EXTERNAL_STORAGE 需在 AndroidManifest.xml 中声明,并在 Google Play 上提交理由审核(仅限必要场景,如文件管理器类应用)。
文件路径与写入方式优化
原代码使用已废弃的 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC),在分区存储下不可写。修复后改用 Environment.getExternalStorageDirectory()(虽不推荐,但为兼容旧逻辑且满足铃声场景可接受),并确保父目录创建逻辑健壮:
File file = new File(Environment.getExternalStorageDirectory() + "/AndRody", N2);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs(); // 递归创建目录
}
同时,I/O 操作升级为 try-with-resources,避免资源泄漏:
Android文件存取与数据库编程知识,文件操作主要是读文件、写文件、读取静态文件等,同时还介绍了创建添加文件内容并保存,打开文件并显示内容;数据库编程方面主要介绍了SQLite数据库的使用、包括创建、删除、打开数据库、非查询SQL操作指令、查询SQL指令-游标Cursors等知识。
try (InputStream inputStream = contentResolver.openInputStream(parse);
OutputStream outputStream = new FileOutputStream(file)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
-
MediaStore 插入字段标准化
原代码使用已弃用的 _data、_size 等私有字段,新版本必须使用 MediaStore.Audio.Media 提供的常量:
ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.Audio.Media.DATA, file.getAbsolutePath()); // 替代 "_data" contentValues.put(MediaStore.Audio.Media.TITLE, N1); // 替代 "title" contentValues.put(MediaStore.Audio.Media.MIME_TYPE, "audio/mpeg"); // 替代 "mime_type" contentValues.put(MediaStore.Audio.Media.SIZE, file.length()); // 替代 "_size" contentValues.put(MediaStore.Audio.Media.IS_RINGTONE, true); // 替代 "is_ringtone"
⚠️ 重要提醒:
- MediaStore.Audio.Media.getContentUriForPath() 在 Android 10+ 返回 null,不可再使用。正确做法是直接向 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 插入(系统自动处理路径映射):
Uri uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues);
- 设置默认铃声时,RingtoneManager.setActualDefaultRingtoneUri() 的第三个参数应为插入后返回的 Uri(即 uri),而非依赖 getContentUriForPath()。
? 完整清单检查
| 项目 | 要求 | 示例 |
|---|---|---|
| Manifest 声明 |
|
必须按 SDK 分级声明 |
| targetSdkVersion | 建议设为 33 或 34,启用分区存储强制模式 | android:targetSdkVersion="34" |
| 权限请求时机 | requestPermission() 中根据 Build.VERSION.SDK_INT 分支调用不同流程 | 见上方修复代码 |
| 媒体扫描(可选) | 插入后可发送广播触发媒体扫描:sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); | 提升文件可见性 |
通过以上改造,应用即可在 Android 9 至 Android 14 全系列版本中稳定完成铃声文件写入、媒体库注册及设为默认铃声操作。核心原则是:放弃路径直写思维,拥抱 MediaStore 标准化接口;按 SDK 版本精准申请对应权限;杜绝私有字段与已弃用 API。










