Android11文件分区存储在图片读写的适配

说明

当APP目标版本是Android 10(API 29)及以后时,由于Android引入了分区存储,APP不能直接通过路径访问文件,访问外部存储空间中的媒体文件除了需要READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 权限之外,需要通过其他APP分享的Uri读写文件,同理要给其余APP分享文件也许要通过FileProvider生成Uri并赋予对应的权限。

本文以从相册中获取图片、请求系统裁剪并返回图片为例展示对应的适配方法。

实际操作

1.从相册中获取图片

从相册中获取到的图片Uri一般如:content://raw//storage/emulated/0/DCIM/Camera/IMG_20210531_183008.HEIC

app内部要读取其内容的话,可以通过context.getContentResolver().openInputStream(imageUri)

或者

1
2
3
4
5
6
Cursor cursor = context.getContentResolver().query(uri, filePathColumn, null, null, null);//从系统表中查询指定Uri对应的照片
cursor.moveToFirst();
int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
if (columnIndex >= 0) {
picturePath = cursor.getString(columnIndex);
}

等方式读取,操作该图片。

2.将外部文件保存到本地并获取Uri

由于上述方式获取到的Uri只对本APP赋予了权限,要是希望将此图片分享给第三方APP进一步加工处理,则可能出现第三方APP没有读写权限而导致操作失败的情况,为了避免这种情况,可以将获取到的图片缓存到APP私有目录,并且重新生成Uri并赋予将要处理该图的第三方APP对应权限。

将外部文件缓存本地的步骤参考第一步操作即可自行完成,主要讲解一下如何将对外分享的Uri赋予读写权限。

下面这个方法在不同系统分别采用不同方式获取文件对应的Uri

1
2
3
4
5
6
7
public static Uri getUriForFile(Context context, File file) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return FileProvider.getUriForFile(context, "com.your.app.packagename.fileprovider", file);
} else {
return Uri.fromFile(file);
}
}

其中com.your.app.packagename.fileproviderFileProviderauthorities

要使用FileProvider可以参考定义FileProvider操作,一般只需要修改authorities即可,同时如果是开发Android库,为了避免与主工程已有的FileProvider冲突,可以继承FileProvider类并修改下文中name字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<manifest>
...
<application>
...
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.mydomain.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
...
</application>
</manifest>

同时,为了定义此FileProvider可以使用的文件目录范围,可以在res/xml文件夹中新建file_paths.xml并做如下配置,也可参考官方文档或者Android N 7.0 FileProvider 兼容适配 原理解析

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<paths>
<root-path
name="camera_photos"
path="" />
<external-files-path // 对应Context#getExternalFilesDir(String)获取的路径,一般为存储卡中Android/data/com.your.app.packagename/file下面的目录
name="external_files"
path="." />
</paths>

3.对外分享有权限的Uri

对于上述步骤获取到的图片Uri赋予权限有两种方式:

第一种,通过Intent传递出去的imgUri,可以使用以下方式:

1
2
3
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.setDataAndType(imgUri, "image/*");

但是这种只适用于主动分享出去的文件,在调用第三方APP裁剪的场景中,一般还需要一个outPutUri用于保存裁剪之后的图片,对于这种场景,可以查询可能会调用的APP并赋予其访问权限:

1
2
3
4
5
6
7
8
9
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
List<ResolveInfo> resInfoList = context.getPackageManager()
.queryIntentActivities(intent, PackageManager.MATCH_ALL);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
contextWrap.getActivity().grantUriPermission(packageName, outPutUri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}

这样不管是分享出去的原图,还是裁剪之后保存的图片都给第三方APP赋予了权限,保证其可以正常访问。

参考文献

https://developer.android.google.cn/reference/androidx/core/content/FileProvider

Android N 7.0 FileProvider 兼容适配 原理解析