Android 13适配踩坑记:从MANAGE_EXTERNAL_STORAGE到READ_MEDIA_IMAGES,我的权限申请血泪史
Android 13权限适配实战从MANAGE_EXTERNAL_STORAGE到READ_MEDIA_IMAGES的完整指南去年夏天当我第一次在Pixel 6上测试我们的应用时那些突如其来的崩溃日志让我意识到Android 13的权限变革远比想象中来得猛烈。作为一款依赖相册和文件管理的社交应用我们不得不面对从MANAGE_EXTERNAL_STORAGE到READ_MEDIA_IMAGES的全面适配挑战。本文将分享这段充满惊喜的适配历程带你避开那些让我熬了三个通宵的深坑。1. Android存储权限的演进与现状记得2019年Android 10引入作用域存储时很多开发者还抱着观望态度。但到了Android 13这套机制已经成为不可回避的现实。最近Google Play的统计显示超过38%的设备已经运行Android 12或更高版本这意味着适配新权限模型不再是可选项。关键变化时间线Android 6.0API 23引入运行时权限Android 10API 29作用域存储试点Android 11API 30强制作用域存储Android 13API 33媒体权限三权分立在适配过程中我发现最令人困惑的是不同版本间的兼容性处理。比如这段判断逻辑就让我栽过跟头when { Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU - { // Android 13处理逻辑 requestPermissions(requireActivity(), arrayOf(READ_MEDIA_IMAGES), REQ_CODE_MEDIA) } Build.VERSION.SDK_INT Build.VERSION_CODES.R - { // Android 11-12处理逻辑 if (!Environment.isExternalStorageManager()) { val intent Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) startActivity(intent) } } else - { // Android 5-10处理逻辑 requestPermissions(requireActivity(), arrayOf(READ_EXTERNAL_STORAGE), REQ_CODE_LEGACY) } }警告MANAGE_EXTERNAL_STORAGE权限在Google Play审核时会被严格审查除非你的应用是文件管理器或备份工具否则很可能被拒绝上架。2. Android 13媒体权限的精细化管理Android 13将媒体访问权限拆分为三个独立部分这个改动让我们的相册模块几乎重写。最坑的是有些设备在升级到Android 13后之前授予的READ_EXTERNAL_STORAGE权限会自动映射而有些则不会——这导致我们收到了大量突然无法访问相册的用户投诉。媒体权限对照表功能需求Android 11-12权限Android 13权限访问图片READ_EXTERNAL_STORAGEREAD_MEDIA_IMAGES访问视频READ_EXTERNAL_STORAGEREAD_MEDIA_VIDEO访问音频READ_EXTERNAL_STORAGEREAD_MEDIA_AUDIO所有媒体文件MANAGE_EXTERNAL_STORAGE需申请全部三种权限实际开发中我推荐使用这个工具类来简化权限检查object MediaPermissionHelper { RequiresApi(Build.VERSION_CODES.TIRAMISU) fun hasImagePermission(context: Context) ContextCompat.checkSelfPermission(context, READ_MEDIA_IMAGES) PERMISSION_GRANTED RequiresApi(Build.VERSION_CODES.TIRAMISU) fun hasVideoPermission(context: Context) ContextCompat.checkSelfPermission(context, READ_MEDIA_VIDEO) PERMISSION_GRANTED fun hasLegacyStoragePermission(context: Context) ContextCompat.checkSelfPermission(context, READ_EXTERNAL_STORAGE) PERMISSION_GRANTED fun shouldShowRationale(activity: Activity, permission: String) ActivityCompat.shouldShowRequestPermissionRationale(activity, permission) }3. 相机权限的特别注意事项相机权限看似简单但在Android 13上却有几个隐藏陷阱。首先是在AndroidManifest中声明的变化!-- 必须同时声明uses-feature -- uses-permission android:nameandroid.permission.CAMERA / uses-feature android:nameandroid.hardware.camera / uses-feature android:nameandroid.hardware.camera.autofocus /更棘手的是运行时处理。我们发现某些OEM厂商的设备会默认拒绝后台相机访问即使用户已经授予权限。解决方案是添加前台服务类型val intent Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, photoUri) flags Intent.FLAG_GRANT_WRITE_URI_PERMISSION } startForegroundService( Intent(this, CameraService::class.java).apply { action ACTION_START_CAMERA putExtra(EXTRA_CAMERA_INTENT, intent) } )常见相机问题排查清单检查Manifest是否正确定义了uses-feature确保相机意图包含FLAG_GRANT_WRITE_URI_PERMISSION在Android 10上使用FileProvider处理文件路径对Android 11处理包可见性过滤4. 优雅降级与兼容性处理面对Android碎片化现实我们的权限代码需要像俄罗斯套娃一样层层嵌套。以下是我总结的版本兼容最佳实践使用RequiresApi精确控制RequiresApi(Build.VERSION_CODES.TIRAMISU) private fun requestMediaPermissions() { requestPermissions( arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VIDEO), REQ_CODE_MEDIA ) }创建权限请求的封装类class PermissionRequestBuilder(private val fragment: Fragment) { private val pendingRequests mutableMapOfInt, (Boolean) - Unit() fun request( permissions: ArrayString, requestCode: Int, callback: (Boolean) - Unit ) { pendingRequests[requestCode] callback fragment.requestPermissions(permissions, requestCode) } fun onResult(requestCode: Int, grantResults: IntArray) { pendingRequests[requestCode]?.invoke( grantResults.all { it PERMISSION_GRANTED } ) } }统一处理权限拒绝的UI流程fun showPermissionDeniedDialog( context: Context, message: String, onConfirm: () - Unit ) { MaterialAlertDialogBuilder(context) .setTitle(权限申请) .setMessage(message) .setPositiveButton(去设置) { _, _ - context.startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data Uri.fromParts(package, context.packageName, null) }) onConfirm() } .setNegativeButton(取消, null) .show() }5. 实战相册模块的完整权限流程让我们通过一个真实的相册选择器案例看看如何串联上述知识点。这个实现支持从Android 5到Android 13的全版本兼容class ImagePickerFragment : Fragment() { private lateinit var permissionBuilder: PermissionRequestBuilder override fun onViewCreated(view: View, savedInstanceState: Bundle?) { permissionBuilder PermissionRequestBuilder(this) btnSelectImage.setOnClickListener { checkPermissionsAndPickImage() } } private fun checkPermissionsAndPickImage() { when { Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU - { if (MediaPermissionHelper.hasImagePermission(requireContext())) { openImagePicker() } else { requestMediaPermissions() } } Build.VERSION.SDK_INT Build.VERSION_CODES.R - { if (Environment.isExternalStorageManager()) { openImagePicker() } else { requestManageStoragePermission() } } else - { if (MediaPermissionHelper.hasLegacyStoragePermission(requireContext())) { openImagePicker() } else { requestLegacyPermissions() } } } } RequiresApi(Build.VERSION_CODES.TIRAMISU) private fun requestMediaPermissions() { permissionBuilder.request( arrayOf(READ_MEDIA_IMAGES), REQ_CODE_MEDIA ) { granted - if (granted) openImagePicker() else showDeniedDialog(需要相册权限来选择图片) } } private fun openImagePicker() { val intent Intent(Intent.ACTION_PICK).apply { type image/* putExtra(Intent.EXTRA_MIME_TYPES, arrayOf(image/jpeg, image/png)) } startActivityForResult(intent, REQ_CODE_PICK_IMAGE) } override fun onRequestPermissionsResult( requestCode: Int, permissions: Arrayout String, grantResults: IntArray ) { permissionBuilder.onResult(requestCode, grantResults) } }这个实现有几个值得注意的细节使用统一的PermissionRequestBuilder处理回调分版本采用不同的权限策略为Android 13单独处理媒体类型过滤提供清晰的用户引导流程6. 调试技巧与测试策略在权限适配过程中有效的调试方法能节省大量时间。以下是我常用的几个技巧ADB命令快速授权/撤销权限# 授予存储权限 adb shell pm grant com.example.app android.permission.READ_EXTERNAL_STORAGE # 撤销相机权限 adb shell pm revoke com.example.app android.permission.CAMERA # 模拟Android 13的媒体权限 adb shell appops set com.example.app READ_MEDIA_IMAGES allow单元测试工具类RunWith(AndroidJUnit4::class) class PermissionUtilsTest { get:Rule val grantPermissionRule GrantPermissionRule.grant( READ_EXTERNAL_STORAGE, CAMERA ) Test fun testHasLegacyStoragePermission() { val context InstrumentationRegistry.getInstrumentation().targetContext assertTrue(PermissionUtils.hasLegacyStoragePermission(context)) } }自动化测试场景覆盖新安装应用时的权限引导流程从低版本升级到高版本的权限迁移用户手动在设置中撤销权限后的恢复流程权限被永久拒绝后的降级体验不同OEM厂商的特殊权限行为记得在开发者选项中开启权限监控功能它可以实时显示应用的所有权限访问记录对排查隐蔽的权限问题特别有用。