
本文详解 android 9(api 28)及以上版本中因存储权限变更、媒体库插入方式调整及外部存储访问限制导致铃声设置功能失效的问题,并提供适配 android 11(api 30)及 android 12+(api 31+)的兼容性修复方案。
本文详解 android 9(api 28)及以上版本中因存储权限变更、媒体库插入方式调整及外部存储访问限制导致铃声设置功能失效的问题,并提供适配 android 11(api 30)及 android 12+(api 31+)的兼容性修复方案。
在 Android 9(Pie)起,系统逐步收紧外部存储访问权限;至 Android 10(API 29),WRITE_EXTERNAL_STORAGE 权限默认失效,应用被强制进入分区存储(Scoped Storage) 模式;而 Android 11(API 30)进一步废弃 MediaStore.Audio.Media.getContentUriForPath() 方法,Android 12(API 31)则要求获取 “所有文件访问权限”(MANAGE_EXTERNAL_STORAGE) 才能写入公共目录(如 Music/)。原代码在新系统上失败的根本原因在于:
- ❌ 继续使用已弃用的 Environment.getExternalStoragePublicDirectory() + WRITE_EXTERNAL_STORAGE;
- ❌ 在 Android 11+ 中仍调用 getContentUriForPath() —— 该方法返回 null,导致 contentResolver.insert() 失败;
- ❌ 未正确申请 MANAGE_EXTERNAL_STORAGE 权限(Android 11+ 必需),且未处理 Environment.isExternalStorageManager() 检查;
- ❌ 文件 I/O 未使用 try-with-resources,存在资源泄漏风险;
- ❌ ContentValues 键名使用硬编码字符串(如 "_data"),应改用 MediaStore.Audio.Media.DATA 等标准常量。
✅ 正确适配方案(支持 Android 9–14)
1. 动态权限申请逻辑分层处理
private void requestPermission() {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// 用户曾拒绝过,引导至应用设置页
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // Android 11+
try {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
} catch (ActivityNotFoundException e) {
// 部分厂商 ROM 使用旧版 Intent Action
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);
}
} else {
// Android 6.0–10:申请传统存储权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_CODE);
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+:检查 MANAGE_EXTERNAL_STORAGE 是否已授予
if (!Environment.isExternalStorageManager()) {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
}
}
}
}
⚠️ 注意:MANAGE_EXTERNAL_STORAGE 权限需在 AndroidManifest.xml 中声明,并通过 Google Play 审核豁免(仅限特定用例,如文件管理器类应用)。普通铃声应用建议优先采用 MediaStore API(见下文替代方案)。
2. 铃声写入与媒体库注册(兼容 Android 10+)
关键修改点:
Android文件存取与数据库编程知识,文件操作主要是读文件、写文件、读取静态文件等,同时还介绍了创建添加文件内容并保存,打开文件并显示内容;数据库编程方面主要介绍了SQLite数据库的使用、包括创建、删除、打开数据库、非查询SQL操作指令、查询SQL指令-游标Cursors等知识。
- ✅ 使用 Environment.getExternalStorageDirectory()(Android 11+ 需配合 MANAGE_EXTERNAL_STORAGE);
- ✅ 替换 getContentUriForPath() 为 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
- ✅ 使用标准 MediaStore 常量填充 ContentValues;
- ✅ 采用 try-with-resources 确保流安全关闭。
private void set_tone(String title, String fileName) {
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
audioManager.setRingerMode(AudioManager.RINGER_MODE_NORMAL);
// 创建目标文件(如 /sdcard/AndRody/{fileName})
File dir = new File(Environment.getExternalStorageDirectory(), "AndRody");
File file = new File(dir, fileName);
dir.mkdirs(); // 自动创建父目录
try {
if (!file.exists()) file.createNewFile();
} catch (IOException e) {
Toast.makeText(this, "创建文件失败", Toast.LENGTH_SHORT).show();
return;
}
// 从 raw 资源复制音频数据
Uri resourceUri = Uri.parse("android.resource://" + getPackageName() + "/raw/" + fileName);
try (InputStream is = getContentResolver().openInputStream(resourceUri);
OutputStream os = new FileOutputStream(file)) {
byte[] buffer = new byte[4096];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "复制音频失败", Toast.LENGTH_SHORT).show();
return;
}
// 插入 MediaStore(推荐方式,无需 MANAGE_EXTERNAL_STORAGE)
ContentValues values = new ContentValues();
values.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName);
values.put(MediaStore.Audio.Media.TITLE, title);
values.put(MediaStore.Audio.Media.MIME_TYPE, "audio/mpeg");
values.put(MediaStore.Audio.Media.IS_RINGTONE, true);
values.put(MediaStore.Audio.Media.IS_NOTIFICATION, false);
values.put(MediaStore.Audio.Media.IS_ALARM, false);
values.put(MediaStore.Audio.Media.IS_MUSIC, false);
// ✅ 关键:使用 MediaStore.EXTERNAL_CONTENT_URI 替代已废弃的 getContentUriForPath()
Uri uri = getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
try (OutputStream out = getContentResolver().openOutputStream(uri)) {
Files.copy(file.toPath(), out);
// 设置为默认铃声
RingtoneManager.setActualDefaultRingtoneUri(
this, RingtoneManager.TYPE_RINGTONE, uri);
Toast.makeText(this, "铃声设置成功", Toast.LENGTH_LONG).show();
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "写入媒体库失败", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(this, "媒体库插入失败", Toast.LENGTH_SHORT).show();
}
}
3. 清单文件配置(必要补充)
<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxsdkversion="29"></uses-permission><!-- 仅 Android 10 及以下生效 --><uses-permission android:name="android.permission.POST_NOTIFICATIONS"></uses-permission><!-- Android 13+ 需要 --><uses-permission android:name="android.permission.READ_MEDIA_AUDIO"></uses-permission><!-- Android 13+ 新权限 --><uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" android:required="false"></uses-permission><!-- 仅 Android 11+ 且必须声明 android:requestLegacyExternalStorage="false" -->
? 总结:
- Android 10+ 推荐完全放弃 getExternalStoragePublicDirectory(),改用 MediaStore API 写入媒体库,既规避权限问题,又符合隐私沙盒规范;
- 若必须访问任意路径(如导出到用户指定目录),则需申请 MANAGE_EXTERNAL_STORAGE 并通过 Play Store 特殊用途审核;
- 所有文件操作务必使用 try-with-resources 或显式 close();
- ContentValues 字段必须使用 MediaStore.* 常量,避免硬编码字段名引发兼容性崩溃。
遵循上述改造后,应用即可在 Android 9 至最新 Android 14 系统上稳定设置铃声,同时满足 Google Play 政策要求。










