很早之前在Android11上面就适配过所有文件管理权限,这次是海外版升级到Android13,由于选择相册用的是第三方库,组内的同事没有上架Google的经验直接就提交代码,虽然功能没有问题,但是上架的时候被打回了,于是记录一下适配工作.
绝大多数需要共享存储空间访问权限的应用都可以遵循共享媒体文件和共享非媒体文件方面的最佳做法。然而,某些应用的核心用例需要广泛访问设备上的文件,但无法采用注重隐私保护的存储最佳实践高效地访问这些文件。对于这些情况,Android 提供了一种名为“所有文件访问权”的特殊应用访问权限。
例如,防病毒应用的主要用例可能需要定期扫描不同目录中的许多文件。如果此扫描需要反复的用户交互,让其使用系统文件选择器选择目录,就会带来糟糕的用户体验。其他用例(如文件管理器应用、备份和恢复应用以及文档管理应用)也需要考虑类似情况。
此部分为在 Google Play 上发布应用的开发者提供通知。
为了限制对共享存储的广泛访问,Google Play 商店已更新其政策,用来评估以 Android 11(API 级别 30)或更高版本为目标平台且通过 MANAGE_EXTERNAL_STORAGE
权限请求“所有文件访问权”的应用。此政策自 2021 年 5 月起生效。
当应用以 Android 11 或更高版本为目标平台并声明了 MANAGE_EXTERNAL_STORAGE
权限时,Android Studio 会显示图 1 中所示的 lint 警告。此警告会提醒您:“Google Play 商店的一项政策限制了对该权限的使用”。
图 1. Android Studio 中的 Lint 警告,提醒开发者有关 MANAGE_EXTERNAL_STORAGE
权限的 Google Play 政策。
仅当您的应用无法有效利用更有利于保护隐私的 API(如存储访问框架或 Media Store API)时,您才能请求 MANAGE_EXTERNAL_STORAGE
权限。您的应用对该权限的使用必须在允许的使用情形范围内,并且必须与应用的核心功能直接相关。如果您的应用包含与以下任一项类似的用例,可能会请求 MANAGE_EXTERNAL_STORAGE
权限:
由于使用的是io.github.lucksiege:pictureselector这个库,版本为v3.10.9:
PictureSelector.create(this)
.openGallery(SelectMimeType.ofImage())
.setImageEngine(GlideEngine.createGlideEngine())
.forResult(object : OnResultCallbackListener {
override fun onResult(result: ArrayList) {
LogUtils.d("===返回的图片地址为===", result[0]!!.path)
Glide.with(this@MainActivity).load(result[0]?.path).into(ivBg)
}
override fun onCancel() {}
})
PictureSelector.create(this)
.openCamera(SelectMimeType.ofImage())
.forResult(object : OnResultCallbackListener {
override fun onResult(result: ArrayList) {
LogUtils.d("===返回的图片地址为===", result[0]!!.path)
Glide.with(this@MainActivity).load(result[0]?.path).into(ivCamera)
}
override fun onCancel() {}
})
private fun openPhotoAlbum() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
//1.以startActivityForResult方式打开Activity
/* startActivityForResult(
Intent.createChooser(intent, "File Browser"), Constants.FILE_CHOOSER_RESULT_CODE)*/
//2.以launch方式打开相册
photoLaunch.launch("image/*")
}
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" android:maxSdkVersion="32"/>
还是提示申请所有文件管理权限,去github查看图片库的版本,发现新版本已经适配了Android13和所有文件管理权限,于是更新一下图片库的依赖版本.
dependencies {
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.8.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation("io.github.lucksiege:pictureselector:v3.11.1")
implementation("com.github.bumptech.glide:glide:4.15.1")
annotationProcessor("com.github.bumptech.glide:compiler:4.15.1")
implementation("com.blankj:utilcodex:1.31.1")
}
/**
* 加载图片
*
* @param context 上下文
* @param url 资源url
* @param imageView 图片承载控件
*/
@Override
public void loadImage(Context context, String url, ImageView imageView) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return;
}
Glide.with(context)
.load(url)
.into(imageView);
}
@Override
public void loadImageBitmap(@NonNull Context context, @NonNull String url, int maxWidth, int maxHeight, OnCallbackListener<Bitmap> call) {
}
/**
* 加载相册目录封面
*
* @param context 上下文
* @param url 图片路径
* @param imageView 承载图片ImageView
*/
@Override
public void loadAlbumCover(Context context, String url, ImageView imageView) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return;
}
Glide.with(context)
.asBitmap()
.load(url)
.override(180, 180)
.sizeMultiplier(0.5f)
.transform(new CenterCrop(), new RoundedCorners(8))
.placeholder(R.drawable.ps_image_placeholder)
.into(imageView);
}
/**
* 加载图片列表图片
*
* @param context 上下文
* @param url 图片路径
* @param imageView 承载图片ImageView
*/
@Override
public void loadGridImage(Context context, String url, ImageView imageView) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return;
}
Glide.with(context)
.load(url)
.override(200, 200)
.centerCrop()
.placeholder(R.drawable.ps_image_placeholder)
.into(imageView);
}
@Override
public void pauseRequests(Context context) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return;
}
Glide.with(context).pauseRequests();
}
@Override
public void resumeRequests(Context context) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return;
}
Glide.with(context).resumeRequests();
}
private MyImageGlideEngine() {
}
private static final class InstanceHolder {
static final MyImageGlideEngine instance = new MyImageGlideEngine();
}
public static MyImageGlideEngine createGlideEngine() {
return InstanceHolder.instance;
}
package com.example.allfilemanagerdemo.utils;
import android.content.Context;
import android.graphics.Bitmap;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.example.allfilemanagerdemo.R;
import com.luck.picture.lib.engine.ImageEngine;
import com.luck.picture.lib.interfaces.OnCallbackListener;
import com.luck.picture.lib.utils.ActivityCompatHelper;
/**
* @author:luck
* @date:2019-11-13 17:02
* @describe:Glide加载引擎
*/
public class MyImageGlideEngine implements ImageEngine {
/**
* 加载图片
*
* @param context 上下文
* @param url 资源url
* @param imageView 图片承载控件
*/
@Override
public void loadImage(Context context, String url, ImageView imageView) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return;
}
Glide.with(context)
.load(url)
.into(imageView);
}
@Override
public void loadImage(Context context, ImageView imageView, String url, int maxWidth, int maxHeight) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return;
}
Glide.with(context)
.load(url)
.override(maxWidth, maxHeight)
.into(imageView);
}
/**
* 加载相册目录封面
*
* @param context 上下文
* @param url 图片路径
* @param imageView 承载图片ImageView
*/
@Override
public void loadAlbumCover(Context context, String url, ImageView imageView) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return;
}
Glide.with(context)
.asBitmap()
.load(url)
.override(180, 180)
.sizeMultiplier(0.5f)
.transform(new CenterCrop(), new RoundedCorners(8))
.placeholder(R.drawable.ps_image_placeholder)
.into(imageView);
}
/**
* 加载图片列表图片
*
* @param context 上下文
* @param url 图片路径
* @param imageView 承载图片ImageView
*/
@Override
public void loadGridImage(Context context, String url, ImageView imageView) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return;
}
Glide.with(context)
.load(url)
.override(200, 200)
.centerCrop()
.placeholder(R.drawable.ps_image_placeholder)
.into(imageView);
}
@Override
public void pauseRequests(Context context) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return;
}
Glide.with(context).pauseRequests();
}
@Override
public void resumeRequests(Context context) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return;
}
Glide.with(context).resumeRequests();
}
private MyImageGlideEngine() {
}
private static final class InstanceHolder {
static final MyImageGlideEngine instance = new MyImageGlideEngine();
}
public static MyImageGlideEngine createGlideEngine() {
return InstanceHolder.instance;
}
}
private fun initPermission() {
if (checkPermissions()) {
takePhoto()
} else {
requestPermission()
}
}
private fun requestPermission() {
when {
Build.VERSION.SDK_INT >= 33 -> {
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_AUDIO,
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.CAMERA,
),
Constants.REQUEST_CODE_PERMISSIONS
)
}
else -> {
ActivityCompat.requestPermissions(
this,
REQUIRED_PERMISSIONS,
Constants.REQUEST_CODE_PERMISSIONS
)
}
}
}
private fun checkPermissions(): Boolean {
when {
Build.VERSION.SDK_INT >= 33 -> {
val permissions = arrayOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_AUDIO,
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.CAMERA,
)
for (permission in permissions) {
return Environment.isExternalStorageManager()
}
}
else -> {
for (permission in REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(
this,
permission
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
}
}
return true
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:
IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
Constants.REQUEST_CODE_PERMISSIONS -> {
var allPermissionsGranted = true
for (result in grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allPermissionsGranted = false
break
}
}
when {
allPermissionsGranted -> {
// 权限已授予,执行文件读写操作
takePhoto()
}
else -> {
// 权限被拒绝,处理权限请求失败的情况
ToastUtils.showShort("请您打开必要权限")
requestPermission()
}
}
}
}
}
这里有两种方式:
11.1 以startActivityForResult方式打开
private fun openPhotoAlbum() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
//以startActivityForResult方式打开Activity
startActivityForResult(
Intent.createChooser(intent, "File Browser"), Constants.FILE_CHOOSER_RESULT_CODE)
}
11.2 以launch方式打开
private fun openPhotoAlbum() {
//以launch方式打开相册
photoLaunch.launch("image/*")
}
12.1 startActivityResult方式结果回调:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode != RESULT_OK) {
return
}
when (requestCode) {
Constants.FILE_CHOOSER_RESULT_CODE -> {
if (data?.data == null) {
return
}
val imgStr: String = getImageAbsolutePath(this, data.data).toString()
if (!TextUtils.isEmpty(imgStr)) {
val imgUri: Uri? = FileUtils.getUriFromPath(imgStr, application)
if (imgUri != null) {
Glide.with(this@MainActivity).load(imgUri.toString()).into(ivPhoto)
}
}
}
}
}
}
12.2 launch方式结果回调:
private val photoLaunch =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
Glide.with(this@MainActivity).load(uri.toString()).into(ivPhoto)
}
https://gitee.com/jackning_admin/all-file-manager-demo