背景
最近两年适配 Android 新版本的小伙伴应该都发现,Android 对于系统读写权限的管理又收窄了。
Android 13(Tiramisu, API 33)上,废弃了 READ_EXTERNAL_STORAGE
权限,新增 READ_MEDIA_IMAGES
、READ_MEDIA_VIDEO
和 READ_MEDIA_AUDIO
三个权限分别用于控制图片、视频、音频的访问。
Android 14(UpsideDownCake, API 34)上则新增了 READ_MEDIA_VISUAL_USER_SELECTED
权限,用户可以选择性地让应用访问部分图片或视频。
由于系统选择器过于丑陋,相信大多数需要用到图片选择功能的 App,都会自行实现或者接入三方库来内置图片选择器功能,而这个行为在 Android 14 上则会遇到新的问题,当用户仅授权部分图片或视频时,整个操作路径会非常长,即用户首先需要选择授权哪些照片给应用,然后再从应用内置的图片选择器中选择照片。
另一方面,Play Store 对于权限的获取政策也在将来收紧:
Timeline information
We anticipate the following timeline for the rollout of the Photo and Video Permissions policy. Note that this is subject to change; updates will be posted in this article.
- October 2023: We announced the new Photos and Video Permissions policy.
- Mid 2024: Apps with one-time or infrequent use of photos requested to use a system photo picker and remove
READ_MEDIA_IMAGES
andREAD_MEDIA_VIDEO
permissions from their app manifest.- Early 2025: Only apps with broad access core functionality can use
READ_MEDIA_IMAGES
andREAD_MEDIA_VIDEO
permissions.时间表信息
我们预计将按照以下时间表发布“照片和视频权限”政策。请注意,此时间表可能会有变动;届时我们会在本文中发布更新。
- 2023 年 10 月: 我们公布了新的“照片和视频权限”政策。
- 2024 年年中: 如果应用只用一次照片,或者很少用到照片,则必须使用系统照片选择器,并从应用清单中移除
READ_MEDIA_IMAGES
和READ_MEDIA_VIDEO
权限。- 2025 年初: 只有核心功能需要获取广泛访问权限的应用才能使用
READ_MEDIA_IMAGES
和READ_MEDIA_VIDEO
权限。
Photo Picker
好消息是,从 Android 13(Tiramisu, API 33)开始,官方提供了系统级图片选择器 Photo Picker,样式和体验都非常丝滑。最重要的是,无需申请权限,只需几行代码即可轻松接入。
androidx.activity 1.6.0+
首先把 androidx.activity
升级到最新版本:
dependencies {
implementation("androidx.activity:activity:1.8.2")
}
由于项目原因无法升级的,在下文会给出解决方案。
接下来只需使用 Activity Result API 调用即可:
class MainActivity : AppCompatActivity() {
...
private val pickMedia = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {
if (it != null) {
// 选择图片的 Uri
} else {
// 未选择图片
}
}
private fun pickImage() = pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
private fun pickVideo() = pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly))
private fun pickImageOrVideo() = pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo))
private fun pickByMime(mimeType: String) = pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.SingleMimeType(mimeType)))
}
通过向 PickVisualMediaRequest()
中传入不同的类型来构建不同的请求对象。是的,你没看错,我们调用的 PickVisualMediaRequest()
实际上是一个方法,它会在内部帮我们构建同名类的对象:
fun PickVisualMediaRequest(
mediaType: VisualMediaType = ImageAndVideo
) = PickVisualMediaRequest.Builder().setMediaType(mediaType).build()
class PickVisualMediaRequest internal constructor() {
...
class Builder {
...
}
}
VisualMediaType
是一个密封接口,由上面我们示例的几种类型实现,分别对应仅选择图片、仅选择视频、选择图片或视频和仅选择特定的 MIME 类型。
效果如下:
如果想设置多选,Photo Picker 同样支持:
class MainActivity : AppCompatActivity() {
...
private val pickMultipleMedia = registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia(5)) {
if (it.isNotEmpty()) {
// 选择的图片 Uri 集合
} else {
// 未选择图片
}
}
private fun pickImages() = pickMultipleMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
private fun pickVideos() = pickMultipleMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly))
private fun pickImageOrVideos() = pickMultipleMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo))
private fun pickByMimes(mimeType: String) = pickMultipleMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.SingleMimeType(mimeType)))
}
只需把 ActivityResultContract
更换为多选的 PickMultipleVisualMedia
即可,同时可以传入一个整型参数控制选择的文件数量上限。效果如下:
是不是非常简单?
前文提到,Photo Picker 从 Android 13(Tiramisu, API 33)开始支持,这个说法其实并不准确,官方文档中提到,支持 Google Play Service 的设备也是可以更新使用的,在不支持 Photo Picker 的低版本机型中,该库会自动调用 ACTION_OPEN_DOCUMENT
打开系统资源管理器进行选择:
同时,如果你使用多选模式,资源管理器会忽略所设置的数量上限。
官方也提供了一个方法用于判断 Photo Picker 在给定设备上是否可用:
class ActivityResultContracts private constructor() {
...
@RequiresApi(19)
open class PickVisualMedia : ActivityResultContract<PickVisualMediaRequest, Uri?>() {
companion object {
...
@SuppressLint("ClassVerificationFailure", "NewApi")
@JvmStatic
fun isPhotoPickerAvailable(context: Context): Boolean {
return isSystemPickerAvailable() || isSystemFallbackPickerAvailable(context) ||
isGmsPickerAvailable(context)
}
}
...
}
}
androidx.activity 1.5.1-
接下来说说 androidx.activity
在低版本中如何处理,低版本中存在的问题主要是 PickVisualMedia
和 PickMultipleVisualMedia
两个 ActivityResultContract
的缺失,同时连带 isPhotoPickerAvailable()
这个判断方法也无法使用。
我们采用的策略就是当系统版本不低于 Android 13 时,使用 Photo Picker,否则使用系统资源管理器。
实现的方法有很多,我这里简单写两个 ActivityResultContracts
示例,你可以根据实际情况进行扩展:
object PicResultContract {
class PickImage : ActivityResultContract<Void?, Uri?>() {
override fun createIntent(context: Context, input: Void?): Intent {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return Intent(MediaStore.ACTION_PICK_IMAGES).setType("image/*")
}
return Intent(Intent.ACTION_OPEN_DOCUMENT).setType("image/*")
}
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
}
}
class PickImages : ActivityResultContract<Int, List<Uri>>() {
override fun createIntent(context: Context, input: Int): Intent {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return Intent(MediaStore.ACTION_PICK_IMAGES)
.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, input)
.setType("image/*")
}
return Intent(Intent.ACTION_OPEN_DOCUMENT)
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
.setType("image/*")
}
override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> {
val i = intent.takeIf { resultCode == Activity.RESULT_OK }
val data = i?.data
if (data != null) {
return listOf(data)
}
val clipData = i?.clipData
if (clipData != null && clipData.itemCount > 0) {
return (0 until clipData.itemCount).map {
clipData.getItemAt(it).uri
}
}
return emptyList()
}
}
}
示例中的多选仍然参考了高版本中的策略,当使用资源管理器时,所设置的文件数量上线也会被忽略。
使用方法与原来类似(仅做示例使用所以没有完全参照原来的写法复刻,自行改造即可):
class MainActivity : AppCompatActivity() {
...
private val pickImage = registerForActivityResult(PicResultContract.PickImage()) {
if (it != null) {
// 选择图片的 Uri
} else {
// 未选择图片
}
}
private val pickImages = registerForActivityResult(PicResultContract.PickImages()) {
if (it.isNotEmpty()) {
// 选择的图片 Uri 集合
} else {
// 未选择图片
}
}
private fun pickImage() = pickImage.launch(null)
private fun pickImages() = pickImages.launch(5)
}
后记
前阵子 @郭霖 发了一篇文章『Android 14新特性,选择性照片和视频访问授权』,我也在评论区探讨了交互的策略,在支持的设备(或 Android 13+)中使用 Photo Picker,否则使用应用内实现的图片选择器,或许是当下最好的方案。