Android存储权限兼容性:API 21到34的适配方案
Android存储权限机制在API 21到34期间经历了重大变革,从传统的读写权限到分区存储(Scoped Storage)的强制实施,再到MANAGE_EXTERNAL_STORAGE特殊权限的引入。本文基于Seal项目的实现,详细解析各版本权限适配方案,帮助开发者解决跨版本存储访问问题。
权限机制演进概述
Android存储权限的演变可分为三个阶段:
- 传统权限阶段(API 21-28):通过
WRITE_EXTERNAL_STORAGE和READ_EXTERNAL_STORAGE控制所有外部存储访问 - 分区存储过渡(API 29-32):引入分区存储,限制应用只能访问自身沙盒和公共媒体目录
- 精细化权限(API 33+):拆分读写权限,增加媒体类型细分权限
权限声明对比
| API级别 | 声明权限 | 特殊配置 |
|---|---|---|
| 21-28 | WRITE_EXTERNAL_STORAGE | 无需额外配置 |
| 29-32 | WRITE_EXTERNAL_STORAGE + android:requestLegacyExternalStorage="true" | 临时兼容传统存储 |
| 33+ | READ_MEDIA_IMAGES READ_MEDIA_VIDEO READ_MEDIA_AUDIO | 按媒体类型申请权限 |
Seal项目在AndroidManifest.xml中声明了兼容配置:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<application android:requestLegacyExternalStorage="true">
运行时权限请求实现
Seal采用Jetpack Compose的rememberPermissionState API处理运行时权限请求,在DownloadPage.kt中实现:
val storagePermission = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE) { granted ->
if (granted) {
checkNetworkOrDownload()
} else {
ToastUtil.makeToast(R.string.permission_denied)
}
}
// 权限检查逻辑
val checkPermissionOrDownload = {
if (Build.VERSION.SDK_INT > 29 || storagePermission.status == PermissionStatus.Granted) {
checkNetworkOrDownload()
} else {
storagePermission.launchPermissionRequest()
}
}
对于API 33+设备,项目在DownloadDirectoryPreferences.kt中单独处理媒体权限:
val storagePermission = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
val showDirectoryAlert = Build.VERSION.SDK_INT >= 30 &&
!Environment.isExternalStorageManager() &&
(!audioDirectoryText.isValidDirectory() || !videoDirectoryText.isValidDirectory())
分区存储适配方案
文件访问路径处理
Seal通过FileUtil.kt封装了不同API级别的路径处理逻辑:
- API < 29:直接使用
Environment.getExternalStorageDirectory() - API 29+:使用
Context.getExternalFilesDir()或媒体集合API
// 公共下载目录获取逻辑
fun getPublicDownloadDir(context: Context): File {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.path ?: "")
} else {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
}
}
存储访问框架(SAF)集成
对于需要访问非媒体文件或自定义目录的场景,Seal集成了存储访问框架,在DownloadDirectoryPreferences.kt中实现目录选择:
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
uri?.let {
App.updateDownloadDir(uri, editingDirectory)
val path = FileUtil.getRealPath(uri)
when (editingDirectory) {
Directory.AUDIO -> audioDirectoryText = path
Directory.VIDEO -> videoDirectoryText = path
}
}
}
MANAGE_EXTERNAL_STORAGE特殊权限
对于需要完全访问外部存储的场景(如自定义下载目录),Seal支持申请MANAGE_EXTERNAL_STORAGE权限,在DownloadDirectoryPreferences.kt中实现:
if (Build.VERSION.SDK_INT >= 30 && !Environment.isExternalStorageManager()) {
Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
data = Uri.parse("package:" + context.packageName)
context.startActivity(this)
}
}
最佳实践总结
-
权限声明优化
- 按API级别拆分权限声明,避免不必要权限
- 使用
maxSdkVersion限制旧权限作用范围
-
运行时权限策略
- 采用Compose权限API实现响应式权限请求
- 权限请求与用户操作绑定,避免启动时集中请求
-
存储路径管理
- 使用
FileProvider分享文件,避免直接路径访问 - 适配代码示例:provider_paths.xml
- 使用
-
用户体验优化
- 权限申请前提供明确说明
- 权限被拒后引导用户到设置页面
Seal项目通过以上适配策略,实现了从API 21到34的存储权限兼容,相关实现可参考:
- 权限请求:DownloadPage.kt
- 目录管理:DownloadDirectoryPreferences.kt
- 文件操作:FileUtil.kt
通过合理的权限管理和存储策略,Seal在保证功能完整的同时,遵循了Android系统的安全规范,为用户提供了流畅的音视频下载体验。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考






