Android 11 作用域存储权限指南
问题描述
在 Android 11 及以上版本中,应用无法再通过 Environment.getExternalStorageDirectory()
直接访问外部存储中的文件。这是 Android 引入的作用域存储(Scoped Storage)机制带来的重要变化,旨在增强用户隐私保护和数据安全。
如果您的应用之前依赖直接文件路径访问照片和媒体文件,在 Android 11 上可能会遇到权限问题,即使应用看起来在虚拟设备上运行正常。
核心解决方案
1. 使用 MediaStore API(推荐)
对于媒体文件(图片、视频、音频等),应使用 Android 提供的 MediaStore API 进行访问:
// 查询图片文件
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.SIZE
)
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
} else {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
val sortOrder = "${MediaStore.Images.Media.DISPLAY_NAME} ASC"
contentResolver.query(
collection,
projection,
null,
null,
sortOrder
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val name = cursor.getString(nameColumn)
val size = cursor.getInt(sizeColumn)
val contentUri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id
)
// 使用 contentUri 访问文件
}
}
2. 使用存储访问框架 (SAF)
对于用户需要选择特定文件的情况,使用存储访问框架是最佳选择:
// 创建文件请求
const val CREATE_FILE = 1
private fun createFile(pickerInitialUri: Uri) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf" // 根据需求更改 MIME 类型
putExtra(Intent.EXTRA_TITLE, "example.pdf")
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
startActivityForResult(intent, CREATE_FILE)
}
3. 使用应用专用目录
对于应用自己创建的文件,使用应用专用目录:
// 获取应用专属外部存储目录
val appSpecificDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val file = File(appSpecificDir, "my_image.jpg")
权限配置
AndroidManifest.xml 配置
<!-- 读取外部存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 写入权限仅对 Android 10 及以下有效 -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<!-- 谨慎使用 MANAGE_EXTERNAL_STORAGE -->
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
警告
MANAGE_EXTERNAL_STORAGE
权限受到严格限制,仅限于文件管理器、杀毒软件等特定类型的应用。使用此权限的应用可能无法通过 Google Play 审核。
Android 10 兼容性配置
<application
android:requestLegacyExternalStorage="true"
...>
<!-- 应用其他配置 -->
</application>
注意
requestLegacyExternalStorage
仅在 targetSdkVersion 为 29 或以下时有效。当 targetSdkVersion 为 30 及以上时,系统会忽略此标志。
权限请求处理
检查权限
private fun checkStoragePermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager()
} else {
val readPermission = ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
)
val writePermission = ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
readPermission == PackageManager.PERMISSION_GRANTED &&
writePermission == PackageManager.PERMISSION_GRANTED
}
}
请求权限
private fun requestStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.data = Uri.parse("package:${applicationContext.packageName}")
startActivityForResult(intent, REQUEST_CODE_MANAGE_STORAGE)
} catch (e: Exception) {
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivityForResult(intent, REQUEST_CODE_MANAGE_STORAGE)
}
} else {
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
),
REQUEST_CODE_STORAGE
)
}
}
处理权限结果
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_MANAGE_STORAGE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Environment.isExternalStorageManager()) {
// 权限已授予
} else {
// 权限被拒绝
}
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (requestCode == REQUEST_CODE_STORAGE) {
if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED &&
grantResults[1] == PackageManager.PERMISSION_GRANTED) {
// 权限已授予
} else {
// 权限被拒绝
}
}
}
文件访问规则总结
媒体文件访问
权限状态 | 可访问位置 |
---|---|
无 READ 权限 | 应用自己创建的媒体文件、应用专属目录、ASD |
有 READ 权限 | 所有共享文件夹中的媒体文件 |
非媒体文件访问
文件位置 | 访问条件 |
---|---|
应用专属目录 | 始终可访问 |
共享文件夹 (/Download, /Documents) | 仅限应用自己创建的文件 |
其他位置 | 需要使用 SAF 或 MANAGE_EXTERNAL_STORAGE |
最佳实践
- 优先使用 MediaStore API 访问媒体文件
- 使用应用专属目录存储应用私有文件
- 使用 SAF 让用户选择文件
- 尽量避免使用 MANAGE_EXTERNAL_STORAGE
- 做好 Android 不同版本的兼容性处理
结论
Android 11 的作用域存储机制虽然增加了开发复杂度,但显著提升了用户隐私保护。通过采用推荐的 MediaStore API、存储访问框架和应用专属目录方法,您可以构建既符合新规又用户友好的应用。
始终优先考虑用户隐私和数据安全,仅在绝对必要时才考虑使用全盘访问权限,并准备好向应用商店和用户充分说明使用该权限的理由。