28、权限与媒体:READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO
Android 13(API 33)开始,媒体权限经历了一次大手术。说白了,就是把原来那个粗放的 READ_EXTERNAL_STORAGE 权限,拆成了三个更精细的权限:READ_MEDIA_IMAGES、READ_MEDIA_VIDEO 和 READ_MEDIA_AUDIO。
为什么要这么拆?我个人的理解是:用户凭什么要因为一个相册 App 能看照片,就允许它扫描你所有的音频文件?这不合逻辑。Google 终于想明白了这件事。
28.1 权限拆分背后的逻辑
在 Android 12 及之前,你只要申请 READ_EXTERNAL_STORAGE,就能访问设备上几乎所有的媒体文件。用户要么全给,要么不给,没有中间选项。
Android 13 的做法更合理:
- 照片权限:只读图片文件
- 视频权限:只读视频文件
- 音频权限:只读音频文件
每个权限独立申请,独立授权。用户可以选择给照片权限但不给音频权限。嗯,这才是正常的设计思路。
关键点:如果你的 App 只处理图片,就只申请 READ_MEDIA_IMAGES。不要贪心去申请另外两个,否则审核时可能会被驳回。
28.2 声明与适配
首先,在 AndroidManifest.xml 中声明你需要的权限:
<!-- Android 13+ 细粒度媒体权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- 兼容 Android 12 及以下 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
注意 maxSdkVersion="32" 这个属性。我建议你一定要加上。为什么?因为 Android 13 上如果同时声明了旧权限和新权限,系统会忽略旧权限,只处理新权限。但如果你忘了加 maxSdkVersion,在旧设备上可能会引发一些奇怪的行为。
28.3 运行时请求权限
代码层面,请求方式跟以前差不多,但判断逻辑要改:
// 判断是否需要请求权限
private boolean needsRequestMediaPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// Android 13+:检查细粒度权限
return ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED
|| ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_MEDIA_VIDEO) != PackageManager.PERMISSION_GRANTED;
} else {
// Android 12 及以下:检查旧权限
return ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED;
}
}
// 请求权限
private void requestMediaPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissions(new String[]{
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.READ_MEDIA_AUDIO
}, REQUEST_CODE_MEDIA_PERMISSIONS);
} else {
requestPermissions(new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE
}, REQUEST_CODE_MEDIA_PERMISSIONS);
}
}
这里有个细节:不要一次性申请所有权限。如果你的 App 只需要图片和视频,就别把音频权限带上。我在项目中遇到过,有些应用为了省事一股脑全申请,结果用户一看「你一个拍照软件要音频权限干嘛?」直接拒绝,反而影响了正常功能。
28.4 权限回调处理
回调处理也要区分版本:
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_MEDIA_PERMISSIONS) {
// 检查每个权限的授予情况
for (int i = 0; i < permissions.length; i++) {
String permission = permissions[i];
boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
if (granted) {
Log.d("Permission", permission + " 已授予");
} else {
Log.w("Permission", permission + " 被拒绝");
// 这里可以提示用户为什么需要这个权限
}
}
// 检查是否所有需要的权限都已授予
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
// 开始加载媒体文件
loadMediaFiles();
} else {
// 提示用户权限不足
showPermissionDeniedDialog();
}
}
}
小技巧:如果用户拒绝了某个权限,不要反复弹窗。我一般会记录用户拒绝的次数,超过两次就引导用户去系统设置里手动开启。这样用户体验会好很多。
28.5 知识体系结构图
下面这张图展示了 Android 媒体权限的演进和适配策略:
28.6 避坑指南
我总结了一些实际开发中容易踩的坑:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Android 13 上权限弹窗不出现 | 只声明了旧权限 READ_EXTERNAL_STORAGE |
添加新权限声明,移除旧权限的 maxSdkVersion 限制 |
| 用户授权后仍无法读取文件 | 使用了已废弃的 MediaStore.Images.Media.DATA 字段 |
改用 ContentResolver.openInputStream() 读取 |
| 旧设备上崩溃 | 运行时检查了 Android 13 的权限常量 | 添加 Build.VERSION.SDK_INT 版本判断 |
| 权限被用户永久拒绝 | 用户勾选了「不再询问」 | 引导用户去系统设置中手动开启 |
特别注意:我曾经遇到过一个案例,某个社交 App 在 Android 13 上申请了 READ_MEDIA_IMAGES 权限,但用户授权后仍然无法加载图片。排查了半天才发现,问题出在 targetSdkVersion 没有更新到 33。记住:权限行为由 targetSdkVersion 决定,而不是 compileSdkVersion。
28.7 最佳实践总结
最后,我给出几个实际开发中的建议:
- 按需申请:只申请你真正需要的权限。不要因为方便就申请全部三个。
- 版本兼容:同时声明新旧权限,用
maxSdkVersion控制旧权限的作用范围。 - 优雅降级:如果用户拒绝了某个权限,App 应该能正常运行,只是对应的功能不可用。
- 用户引导:权限被拒绝后,给出合理的解释,而不是直接崩溃或弹窗轰炸。
- 测试覆盖:在 Android 12 和 Android 13 的真机上都要测试,模拟器有时候表现不准确。
嗯,关于媒体权限的拆分就讲到这里。说白了,这是 Android 在隐私保护上迈出的一大步。虽然适配起来多了一些工作量,但从用户体验和隐私安全的角度来看,绝对是值得的。