告别敏感权限:Android应用如何实现零权限文件访问?

5 阅读2分钟

导语

Android\text{Android} 应用是否曾因请求 READ_MEDIA_IMAGES\text{READ\_MEDIA\_IMAGES}READ_EXTERNAL_STORAGE\text{READ\_EXTERNAL\_STORAGE} 权限而被 Google\text{Google} Play\text{Play} 拒绝?随着 Android\text{Android} 权限和隐私政策的不断收紧,除非应用的核心功能是文件管理或相册,否则过度索取存储权限将是应用上架的主要障碍。

本文将深入探讨 Google\text{Google} 的最新要求,并提供一套零权限的解决方案,指导开发者使用 Android\text{Android} 照片选择器 (Photo\text{Photo} Picker\text{Picker})Storage\text{Storage} Access\text{Access} Framework\text{Framework} (SAF\text{SAF}) 来安全、合规地访问图片、视频和任意文件。


🎯 Google\text{Google} Play\text{Play} 政策核心变化:应用为什么会被拒?

如果应用在 Manifest\text{Manifest} 文件中声明了以下权限,但核心功能并非需要广泛、持续地访问设备上的所有媒体文件(例如,仅用于上传头像),那么应用的更新版本几乎确定会被 Google\text{Google} 拒绝:

  • READ_MEDIA_IMAGES
  • READ_MEDIA_VIDEO
  • READ_EXTERNAL_STORAGE (在 Android\text{Android} 13\text{13} 及以上基本被上述权限取代)

Google\text{Google} 的要求是:如果需求是用户选择特定文件进行一次性操作,应用应该使用以下两种基于用户授权API\text{API},从而完全避免请求存储权限


一、零权限访问图片和视频:Android\text{Android} Photo\text{Photo} Picker\text{Picker}

对于图片和视频选择,Photo\text{Photo} Picker\text{Picker}Android\text{Android} 13\text{13} ( API\text{API} 33\text{33}) 引入的首选 API\text{API},并且通过 Jetpack\text{Jetpack} 库可以向后兼容至 Android\text{Android} 4.4\text{4.4} (API\text{API} 19\text{19})。

1. 添加依赖

需要使用 Activity\text{Activity}Result\text{Result} API\text{API} 库。

Gradle

dependencies {
    implementation("androidx.activity:activity-ktx:1.9.0") // 或更高版本
}

2. 选取单张图片

以下代码展示了如何启动 Photo\text{Photo} Picker\text{Picker} 并处理返回的 URI\text{URI}全程无需权限声明

Kotlin

import androidx.activity.result.contract.ActivityResultContracts
import android.net.Uri

// 注册 Launcher,用于接收结果
private val pickImage = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri: Uri? ->
    if (uri != null) {
        // 成功!使用 ContentResolver 处理这个 URI
        Log.d("PhotoPicker", "Selected Image URI: $uri")
        // 例如:将 URI 加载到 ImageView 或获取 InputStream
        contentResolver.openInputStream(uri)?.use { inputStream ->
            // 在这里处理图片数据流
        }
    } else {
        Log.d("PhotoPicker", "No image selected")
    }
}

// 启动图片选择器的方法
fun launchPhotoPicker() {
    pickImage.launch(
        PickVisualMediaRequest.Builder()
            // 仅允许选择图片
            .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly) 
            .build()
    )
}

3. 选取多个媒体文件(可选)

如果应用允许用户一次选择多张图片或视频:

Kotlin

// 注册 Launcher,限制最多选择 5 个媒体文件
private val pickMultipleMedia = registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia(5)) { uris: List<Uri> ->
    if (uris.isNotEmpty()) {
        Log.d("PhotoPicker", "Number of items selected: ${uris.size}")
        // 遍历 uris 列表进行处理
        uris.forEach { uri ->
            // 处理每个 URI
        }
    }
}

// 启动多选的方法
fun launchMultiPhotoPicker() {
    pickMultipleMedia.launch(
        PickVisualMediaRequest.Builder()
            // 允许选择图片和视频
            .setMediaType(ActivityResultContracts.PickVisualMedia.ImageAndVideo)
            .build()
    )
}

二、零权限访问任意文件:Storage\text{Storage} Access\text{Access} Framework\text{Framework} (SAF\text{SAF})

如果应用需要用户选择 PDF\text{PDF}Zip\text{Zip}、文本文档或任何其他非媒体文件,则应使用 SAF\text{SAF}

1. 选取任意单个文件

使用 ActivityResultContracts.OpenDocument\text{ActivityResultContracts.OpenDocument} 合约来实现文件的选择。

Kotlin

import androidx.activity.result.contract.ActivityResultContracts
import android.net.Uri
import android.content.Intent

// 注册 Launcher,用于接收文件结果
private val filePickerLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
    if (uri != null) {
        Log.d("SAF", "Selected File URI: $uri")
        
        // 关键:获取持久性访问权限(如果需要长期访问此文件)
        val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
        contentResolver.takePersistableUriPermission(uri, flag)
        
        // 读取文件内容
        contentResolver.openInputStream(uri)?.use { inputStream ->
            // 在这里处理文件数据流(例如,读取文本或字节)
        }
    }
}

// 启动任意文件选择器
fun launchAnyFilePicker() {
    // 使用 "*/*" MIME 类型来允许用户选择任意类型的文件
    filePickerLauncher.launch("*/*")
}

2. 选取特定类型文件(可选)

如果应用只想让用户选择特定 MIME\text{MIME} 类型的文件(例如,PDF\text{PDF}):

Kotlin

fun launchPdfFilePicker() {
    // 启动 PDF 文件选择器
    filePickerLauncher.launch("application/pdf")
}

结论

通过从应用的 Manifest\text{Manifest} 文件中删除 READ_MEDIA_*\text{READ\_MEDIA\_*} / READ_EXTERNAL_STORAGE\text{READ\_EXTERNAL\_STORAGE} 等权限,并用上述 Photo\text{Photo} Picker\text{Picker}SAF\text{SAF} 机制替换自定义文件选择逻辑,应用不仅能解决 Google\text{Google} Play\text{Play} 的权限拒绝问题,还能使应用更加安全、更符合现代 Android\text{Android} 的隐私设计原则。

建议开发者立即行动,使用 Android\text{Android} 官方推荐的 API\text{API} 更新应用,确保顺利通过审核!