28、权限与媒体:READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO

Android 13(API 33)开始,媒体权限经历了一次大手术。说白了,就是把原来那个粗放的 READ_EXTERNAL_STORAGE 权限,拆成了三个更精细的权限:READ_MEDIA_IMAGESREAD_MEDIA_VIDEOREAD_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 媒体权限的演进和适配策略:

Android 媒体权限适配策略 Android 12 及以下 权限:READ_EXTERNAL_STORAGE 一个权限覆盖所有媒体文件 声明方式 直接声明,无 maxSdkVersion 限制 用户授权 要么全给,要么不给 ⚠ 粗粒度,隐私保护较弱 Android 13+ READ_MEDIA_IMAGES 仅读取图片文件 READ_MEDIA_VIDEO 仅读取视频文件 READ_MEDIA_AUDIO 仅读取音频文件 ✓ 细粒度,用户可精确控制 适配方向 建议:同时声明新旧权限,用 maxSdkVersion 控制兼容性

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 最佳实践总结

最后,我给出几个实际开发中的建议:

  1. 按需申请:只申请你真正需要的权限。不要因为方便就申请全部三个。
  2. 版本兼容:同时声明新旧权限,用 maxSdkVersion 控制旧权限的作用范围。
  3. 优雅降级:如果用户拒绝了某个权限,App 应该能正常运行,只是对应的功能不可用。
  4. 用户引导:权限被拒绝后,给出合理的解释,而不是直接崩溃或弹窗轰炸。
  5. 测试覆盖:在 Android 12 和 Android 13 的真机上都要测试,模拟器有时候表现不准确。

嗯,关于媒体权限的拆分就讲到这里。说白了,这是 Android 在隐私保护上迈出的一大步。虽然适配起来多了一些工作量,但从用户体验和隐私安全的角度来看,绝对是值得的。


公众号:蓝海资料掘金营,微信deep3321