前言
大家应该都熟悉FileProvider吧,但是其诞生的原因,内部怎么实现的,又是怎么转化为文件的,大家有了解多少呢?今天就通过它重新看看ContentProvider这个四大组件之一。
在Android7.0,Android提高了应用的隐私权,限制了在应用间共享文件。如果需要在应用间共享,需要授予要访问的URI临时访问权限。
以下是官方说明:
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。”
为什么限制在应用间共享文件
打个比方,应用A有一个文件,绝对路径为file:///storage/emulated/0/Download/photo.jpg
现在应用A想通过其他应用来完成一些需求,比如拍照,就把他的这个文件路径发给了照相应用B,然后应用B照完相就把照片存储到了这个绝对路径。
看起来似乎没有什么问题,但是如果这个应用B是个“坏应用”呢?
如果这个应用A是“坏应用”呢?
可以看到,这个之前落伍的方案,从自身到对方,都是不太好的选择。
所以Google就想了一个办法,把对文件的访问限制在应用内部。
也就是应用A分享相对路径给应用B,应用B拿着这个相对路径找到应用A,应用A读取文件内容返给应用B。
配置FileProvider
搞清楚了要做什么事,接下来就是怎么做。
涉及到应用间通信的问题,还记得IPC的几种方式吗?
从易用性,安全性,完整度等各个方面考虑,Google选择了ContentProvider为这次限制应用分享文件的 解决方案。于是,FileProvider诞生了。
具体做法就是:
- <!-- 配置FileProvider-->
- <provider
- android:name="androidx.core.content.FileProvider"
- android:authorities="${applicationId}.provider"
- android:exported="false"
- android:grantUriPermissions="true">
- <meta-data
- android:name="android.support.FILE_PROVIDER_PATHS"
- android:resource="@xml/provider_paths"/>
- </provider>
- <?xml version="1.0" encoding="utf-8"?>
- <paths xmlns:android="http://schemas.android.com/apk/res/android">
- <external-path name="external" path="."/>
- </paths>
- //修改文件URL获取方式
- Uri photoURI = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", createImageFile());
这样配置之后,就能生成content:// URI,并且也能通过这个URI来传输文件内容给外部应用。
FileProvider这些配置属性也就是ContentProvider的通用配置:
其中要注意的是android:exported正常应该是true,因为要给外部应用使用。
但是FileProvider这里设置为false,并且必须为false。
这主要为了保护应用隐私,如果设置为true,那么任何一个应用都可以来访问当前应用的FileProvider了,对于应用文件来说不是很可取,所以Android7.0以上会通过其他方式让外部应用安全的访问到这个文件,而不是普通的ContentProvider访问方式,后面会说到。
也正是因为这个属性为true,在Android7.0以下,Android默认是将它当成一个普通的ContentProvider,外部无法通过content:// URI来访问文件。所以一般要判断下系统版本再确定传入的Uri到底是File格式还是content格式。
FileProvider源码
接着看看FileProvider的主要源码:
- public class FileProvider extends ContentProvider {
- @Override
- public boolean onCreate() {
- return true;
- }
- @Override
- public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
- super.attachInfo(context, info);
- // Sanity check our security
- if (info.exported) {
- throw new SecurityException("Provider must not be exported");
- }
- if (!info.grantUriPermissions) {
- throw new SecurityException("Provider must grant uri permissions");
- }
- mStrategy = getPathStrategy(context, info.authority);
- }
- public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
- @NonNull File file) {
- final PathStrategy strategy = getPathStrategy(context, authority);
- return strategy.getUriForFile(file);
- }
- @Override
- public Uri insert(@NonNull Uri uri, ContentValues values) {
- throw new UnsupportedOperationException("No external inserts");
- }
- @Override
- public int update(@NonNull Uri uri, ContentValues values, @Nullable String selection,
- @Nullable String[] selectionArgs) {
- throw new UnsupportedOperationException("No external updates");
- }
- @Override
- public int delete(@NonNull Uri uri, @Nullable String selection,
- @Nullable String[] selectionArgs) {
- // ContentProvider has already checked granted permissions
- final File file = mStrategy.getFileForUri(uri);
- return file.delete() ? 1 : 0;
- }
- @Override
- public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
- @Nullable String[] selectionArgs,
- @Nullable String sortOrder) {
- // ContentProvider has already checked granted permissions
- final File file = mStrategy.getFileForUri(uri);
- if (projection == null) {
- projection = COLUMNS;
- }
- String[] cols = new String[projection.length];
- Object[] values = new Object[projection.length];
- int i = 0;
- for (String col : projection) {
- if (OpenableColumns.DISPLAY_NAME.equals(col)) {
- cols[i] = OpenableColumns.DISPLAY_NAME;
- values[i++] = file.getName();
- } else if (OpenableColumns.SIZE.equals(col)) {
- cols[i] = OpenableColumns.SIZE;
- values[i++] = file.length();
- }
- }
- cols = copyOf(cols, i);
- values = copyOf(values, i);
- final MatrixCursor cursor = new MatrixCursor(cols, 1);
- cursor.addRow(values);
- return cursor;
- }
- @Override
- public String getType(@NonNull Uri uri) {
- final File file = mStrategy.getFileForUri(uri);
- final int lastDot = file.getName().lastIndexOf('.');
- if (lastDot >= 0) {
- final String extension = file.getName().substring(lastDot + 1);
- final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
- if (mime != null) {
- return mime;
- }
- }
- return "application/octet-stream";
- }
- }
任何一个ContentProvider都需要继承ContentProvider类,然后实现这几个抽象方法:
onCreate,getType,query,insert,delete,update。
(其中每个方法中的Uri参数,就是我们之前通过getUriForFile方法生成的content URI)
我们分三部分说说:
数据调用方面
其中,query,insert,delete,update四个方法就是数据的增删查改,也就是进程间通信的相关方法。
其他应用可以通过ContentProvider来调用这几个方法,来完成对本地应用数据的增删查改,从而完成进程间通信的功能。
具体方法就是调用getContentResolver()的相关方法,例如:
- Cursor cursor = getContentResolver().query(uri, null, null, null, "userid");
再回去看看FileProvider:
MIME类型
再看getType方法,这个方法主要是返回 Url所代表数据的MIME类型。
一般是使用默认格式:
具体怎么用呢?可以通过Content URI对应的ContentProvider配置的getType来匹配Activity。
有点拗口,比如Activity和ContentProvider这么配置的:
- <activity
- android:name=".SecondActivity">
- <intent-filter>
- <action android:name=""/>
- <category android:name=""/>
- <data android:mimeType="type_test"/>
- </intent-filter>
- </activity>
- @Override
- public String getType(@NonNull Uri uri) {
- return "type_test";
- }
- intent.setData(mContentRUI);
- startActivity(intent)
这样配置之后,startActivity就会检查Activity的mineType 和 Content URI 对应的ContentProvider的getType是否相同,相同情况下才能正常打开Activity。
初始化
最后再看看onCreate方法。
在APP启动流程中,自动执行所有ContentProvider的attachInfo方法,并最后调用到onCreate方法。一般在这个方法中就做一些初始化工作,比如初始化ContentProvider所需要的数据库。
而在FileProvider中,调用了attachInfo方法作为了一个初始化工作的入口,其实和onCreate方法的作用一样,都是App启动的时候会调用的方法。
在这个方法中,也是限制了exported属性必须为false,grantUriPermissions属性必须为true。
- if (info.exported) {
- throw new SecurityException("Provider must not be exported");
- }
- if (!info.grantUriPermissions) {
- throw new SecurityException("Provider must grant uri permissions");
- }
这个初始化方法和特性,也是被很多三方库所利用,可以进行静默无感知的初始化工作,而无需单独调用三方库初始化方法。比如Facebook SDK:
- <provider
- android:name="com.facebook.internal.FacebookInitProvider"
- android:authorities="${applicationId}.FacebookInitProvider"
- android:exported="false" />
- public final class FacebookInitProvider extends ContentProvider {
- private static final String TAG = FacebookInitProvider.class.getSimpleName();
- @Override
- @SuppressWarnings("deprecation")
- public boolean onCreate() {
- try {
- FacebookSdk.sdkInitialize(getContext());
- } catch (Exception ex) {
- Log.i(TAG, "Failed to auto initialize the Facebook SDK", ex);
- }
- return false;
- }
- //...
- }
这样一写,就无需单独集成FacebookSDK的初始化方法了,实现静默初始化。
而Jetpack中的App Startup也是考虑到这些三方库的需求,对三方库的初始化进行了一个合并,从而优化了多次创建ContentProvider的耗时。
拿到Content URI 该怎么使用?
很多人都知道该怎么配置FileProvider让别人(比如照相APP)来获取我们的Content URI,但是你们知道别人拿到Content URI之后又是怎么获取具体的File的呢?
其实仔细找找就能发现,在FileProvider.java中有注释说明:
- The client app that receives the content URI can open the file and access its contents by calling
- {@link android.content.ContentResolver#openFileDescriptor(Uri, String) ContentResolver.openFileDescriptor}
- to get a {@link ParcelFileDescriptor}
也就是openFileDescriptor方法,拿到ParcelFileDescriptor类型数据,其实就是一个文件描述符,然后就可以读取文件流了。
- ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(intent.getData(), "r");
- FileReader reader = new FileReader(parcelFileDescriptor.getFileDescriptor());
- BufferedReader bufferedReader = new BufferedReader(reader);
ContentProvider 实际应用
在平时的工作中,主要有以下以下几种情况和ContentProvider打交道比较多:
总结
ContentProvider作为四大组件之一,似乎并没有其他组件的存在感那么强。
但是他还是有自己的那一份职责,也就是在保证安全的情况下进行应用间通信,还可以扩展作为帮助初始化的组件。所以了解他,掌握它也是很重要的,没准以后哪个时候你就需要他了。
不要忽视任何一个知识点。
参考
https://mp.weixin.qq.com/s/kQmH2GnwW8FK-yNmWcheTA
https://segmentfault.com/a/1190000021357383
https://blog.csdn.net/lmj623565791/article/details/72859156
本文转载自微信公众号「码上积木」,可以通过以下二维码关注。转载本文请联系码上积木公众号。
逛个动物园要指纹打卡,连回家进小区也要刷脸验明正身会议期间,记者在浙江代表...
5G切片是新商业模式的关键推动者,也是增强5G潜力的关键概念。通信服务提供商可...
整个欧洲向智能建筑迈进的步伐正在加快。随着各行各业的组织在客户和员工体验方...
近年来,因高空抛物、坠物造成的伤害事件屡上报端。水瓶、西瓜皮、易拉罐,甚至...
1.终有那么一个人,可以随时改变着你的心情。 2.有的东西你再喜欢也不会属於你...
1.总有一天,我们会过上我一翻身就可以偷亲你的日子。 2.即使一贫如洗,我会是...
5G网络建设加快,超前布局6G 截止目前,我国累计建成的5G基站数量超过71.8万座,...
3月15日消息 一年一度的央视财经 3.15 晚会正在进行中,从前言来看主要曝光问题...
iOS 11~iOS 14.3的越狱工具发布了un0ver6.0.0版本 支持iOS11-iOS 14.3系统设备进...
人脸解锁扫脸支付随着人脸识别技术的不断发展,如今借助一个小小的摄像头就能让...