Skip to content

Android 11 作用域存储权限指南

问题描述

在 Android 11 及以上版本中,应用无法再通过 Environment.getExternalStorageDirectory() 直接访问外部存储中的文件。这是 Android 引入的作用域存储(Scoped Storage)机制带来的重要变化,旨在增强用户隐私保护和数据安全。

如果您的应用之前依赖直接文件路径访问照片和媒体文件,在 Android 11 上可能会遇到权限问题,即使应用看起来在虚拟设备上运行正常。

核心解决方案

1. 使用 MediaStore API(推荐)

对于媒体文件(图片、视频、音频等),应使用 Android 提供的 MediaStore API 进行访问:

kotlin
// 查询图片文件
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)

对于用户需要选择特定文件的情况,使用存储访问框架是最佳选择:

kotlin
// 创建文件请求
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. 使用应用专用目录

对于应用自己创建的文件,使用应用专用目录:

kotlin
// 获取应用专属外部存储目录
val appSpecificDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val file = File(appSpecificDir, "my_image.jpg")

权限配置

AndroidManifest.xml 配置

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 兼容性配置

xml
<application
    android:requestLegacyExternalStorage="true"
    ...>
    <!-- 应用其他配置 -->
</application>

注意

requestLegacyExternalStorage 仅在 targetSdkVersion 为 29 或以下时有效。当 targetSdkVersion 为 30 及以上时,系统会忽略此标志。

权限请求处理

检查权限

kotlin
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
    }
}

请求权限

kotlin
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
        )
    }
}

处理权限结果

kotlin
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

最佳实践

  1. 优先使用 MediaStore API 访问媒体文件
  2. 使用应用专属目录存储应用私有文件
  3. 使用 SAF 让用户选择文件
  4. 尽量避免使用 MANAGE_EXTERNAL_STORAGE
  5. 做好 Android 不同版本的兼容性处理

结论

Android 11 的作用域存储机制虽然增加了开发复杂度,但显著提升了用户隐私保护。通过采用推荐的 MediaStore API、存储访问框架和应用专属目录方法,您可以构建既符合新规又用户友好的应用。

始终优先考虑用户隐私和数据安全,仅在绝对必要时才考虑使用全盘访问权限,并准备好向应用商店和用户充分说明使用该权限的理由。