透过FileProvider再看ContentProvider

移动开发 Android
大家应该都熟悉FileProvider吧,但是其诞生的原因,内部怎么实现的,又是怎么转化为文件的,大家有了解多少呢?今天就通过它重新看看ContentProvider这个四大组件之一。

[[384130]]

前言

大家应该都熟悉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是“坏应用”呢?

  • 自己可以不用申请存储权限,利用应用B就达到了存储文件的这一危险权限。

可以看到,这个之前落伍的方案,从自身到对方,都是不太好的选择。

所以Google就想了一个办法,把对文件的访问限制在应用内部。

  • 如果要分享文件路径,不要分享file:// URI这种文件的绝对路径,而是分享content:// URI,这种相对路径,也就是这种格式:content://com.jimu.test.fileprovider/external/photo.jpg
  • 然后其他应用可以通过这个绝对路径来向文件所属应用 索要 文件数据,所以文件所属的应用本身必须拥有文件的访问权限。

也就是应用A分享相对路径给应用B,应用B拿着这个相对路径找到应用A,应用A读取文件内容返给应用B。

配置FileProvider

搞清楚了要做什么事,接下来就是怎么做。

涉及到应用间通信的问题,还记得IPC的几种方式吗?

  • 文件
  • AIDL
  • ContentProvider
  • Socket
  • 等等。

从易用性,安全性,完整度等各个方面考虑,Google选择了ContentProvider为这次限制应用分享文件的 解决方案。于是,FileProvider诞生了。

具体做法就是:

  1. <!-- 配置FileProvider--> 
  2.  
  3. <provider 
  4.     android:name="androidx.core.content.FileProvider" 
  5.     android:authorities="${applicationId}.provider" 
  6.     android:exported="false" 
  7.     android:grantUriPermissions="true"
  8.     <meta-data 
  9.         android:name="android.support.FILE_PROVIDER_PATHS" 
  10.         android:resource="@xml/provider_paths"/> 
  11. </provider> 
  12.  
  13.  
  14.  
  15. <?xml version="1.0" encoding="utf-8"?> 
  16. <paths xmlns:android="http://schemas.android.com/apk/res/android"
  17.     <external-path name="external" path="."/> 
  18. </paths> 
  1. //修改文件URL获取方式 
  2.  
  3. Uri photoURI = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", createImageFile()); 

这样配置之后,就能生成content:// URI,并且也能通过这个URI来传输文件内容给外部应用。

FileProvider这些配置属性也就是ContentProvider的通用配置:

  • android:name,是ContentProvider的类路径。
  • android:authorities,是唯一标示,一般为包名+.provider
  • android:exported,表示该组件是否能被其他应用使用。
  • android:grantUriPermissions,表示是否允许授权文件的临时访问权限。

其中要注意的是android:exported正常应该是true,因为要给外部应用使用。

但是FileProvider这里设置为false,并且必须为false。

这主要为了保护应用隐私,如果设置为true,那么任何一个应用都可以来访问当前应用的FileProvider了,对于应用文件来说不是很可取,所以Android7.0以上会通过其他方式让外部应用安全的访问到这个文件,而不是普通的ContentProvider访问方式,后面会说到。

也正是因为这个属性为true,在Android7.0以下,Android默认是将它当成一个普通的ContentProvider,外部无法通过content:// URI来访问文件。所以一般要判断下系统版本再确定传入的Uri到底是File格式还是content格式。

FileProvider源码

接着看看FileProvider的主要源码:

  1. public class FileProvider extends ContentProvider { 
  2.  
  3.     @Override 
  4.     public boolean onCreate() { 
  5.         return true
  6.     } 
  7.  
  8.     @Override 
  9.     public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) { 
  10.         super.attachInfo(context, info); 
  11.  
  12.         // Sanity check our security 
  13.         if (info.exported) { 
  14.             throw new SecurityException("Provider must not be exported"); 
  15.         } 
  16.         if (!info.grantUriPermissions) { 
  17.             throw new SecurityException("Provider must grant uri permissions"); 
  18.         } 
  19.  
  20.         mStrategy = getPathStrategy(context, info.authority); 
  21.     } 
  22.  
  23.  
  24.     public static Uri getUriForFile(@NonNull Context context, @NonNull String authority, 
  25.             @NonNull File file) { 
  26.         final PathStrategy strategy = getPathStrategy(context, authority); 
  27.         return strategy.getUriForFile(file); 
  28.     } 
  29.  
  30.  
  31.     @Override 
  32.     public Uri insert(@NonNull Uri uri, ContentValues values) { 
  33.         throw new UnsupportedOperationException("No external inserts"); 
  34.     } 
  35.  
  36.  
  37.     @Override 
  38.     public int update(@NonNull Uri uri, ContentValues values, @Nullable String selection, 
  39.             @Nullable String[] selectionArgs) { 
  40.         throw new UnsupportedOperationException("No external updates"); 
  41.     } 
  42.  
  43.  
  44.     @Override 
  45.     public int delete(@NonNull Uri uri, @Nullable String selection, 
  46.             @Nullable String[] selectionArgs) { 
  47.         // ContentProvider has already checked granted permissions 
  48.         final File file = mStrategy.getFileForUri(uri); 
  49.         return file.delete() ? 1 : 0; 
  50.     } 
  51.  
  52.     @Override 
  53.     public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, 
  54.             @Nullable String[] selectionArgs, 
  55.             @Nullable String sortOrder) { 
  56.         // ContentProvider has already checked granted permissions 
  57.         final File file = mStrategy.getFileForUri(uri); 
  58.  
  59.         if (projection == null) { 
  60.             projection = COLUMNS; 
  61.         } 
  62.  
  63.         String[] cols = new String[projection.length]; 
  64.         Object[] values = new Object[projection.length]; 
  65.         int i = 0; 
  66.         for (String col : projection) { 
  67.             if (OpenableColumns.DISPLAY_NAME.equals(col)) { 
  68.                 cols[i] = OpenableColumns.DISPLAY_NAME; 
  69.                 values[i++] = file.getName(); 
  70.             } else if (OpenableColumns.SIZE.equals(col)) { 
  71.                 cols[i] = OpenableColumns.SIZE
  72.                 values[i++] = file.length(); 
  73.             } 
  74.         } 
  75.  
  76.         cols = copyOf(cols, i); 
  77.         values = copyOf(values, i); 
  78.  
  79.         final MatrixCursor cursor = new MatrixCursor(cols, 1); 
  80.         cursor.addRow(values); 
  81.         return cursor
  82.     } 
  83.  
  84.     @Override 
  85.     public String getType(@NonNull Uri uri) { 
  86.   final File file = mStrategy.getFileForUri(uri); 
  87.  
  88.         final int lastDot = file.getName().lastIndexOf('.'); 
  89.         if (lastDot >= 0) { 
  90.             final String extension = file.getName().substring(lastDot + 1); 
  91.             final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 
  92.             if (mime != null) { 
  93.                 return mime; 
  94.             } 
  95.         } 
  96.         return "application/octet-stream"
  97.     } 
  98.  
  99.  

任何一个ContentProvider都需要继承ContentProvider类,然后实现这几个抽象方法:

onCreate,getType,query,insert,delete,update。

(其中每个方法中的Uri参数,就是我们之前通过getUriForFile方法生成的content URI)

我们分三部分说说:

数据调用方面

其中,query,insert,delete,update四个方法就是数据的增删查改,也就是进程间通信的相关方法。

其他应用可以通过ContentProvider来调用这几个方法,来完成对本地应用数据的增删查改,从而完成进程间通信的功能。

具体方法就是调用getContentResolver()的相关方法,例如:

  1. Cursor cursor = getContentResolver().query(uri, nullnullnull"userid");  

再回去看看FileProvider:

  • query,查询方法。在该方法中,返回了File的name和length。
  • insert,插入方法。没有做任何事。
  • delete,删除方法。删除Uri对应的File。
  • update,更新方法。没有做任何事。

MIME类型

再看getType方法,这个方法主要是返回 Url所代表数据的MIME类型。

一般是使用默认格式:

  • 如果是单条记录返回以vnd.android.cursor.item/ 为首的字符串
  • 如果是多条记录返回vnd.android.cursor.dir/ 为首的字符串

具体怎么用呢?可以通过Content URI对应的ContentProvider配置的getType来匹配Activity。

有点拗口,比如Activity和ContentProvider这么配置的:

  1. <activity   
  2.     android:name=".SecondActivity">   
  3.     <intent-filter>   
  4.         <action android:name=""/>   
  5.         <category android:name=""/>   
  6.         <data android:mimeType="type_test"/>  
  7.     </intent-filter>   
  8. </activity>   
  1. @Override 
  2. public String getType(@NonNull Uri uri) { 
  3.     return "type_test"
  4.  
  5.  
  6. intent.setData(mContentRUI);   
  7. startActivity(intent) 

这样配置之后,startActivity就会检查Activity的mineType 和 Content URI 对应的ContentProvider的getType是否相同,相同情况下才能正常打开Activity。

初始化

最后再看看onCreate方法。

在APP启动流程中,自动执行所有ContentProvider的attachInfo方法,并最后调用到onCreate方法。一般在这个方法中就做一些初始化工作,比如初始化ContentProvider所需要的数据库。

而在FileProvider中,调用了attachInfo方法作为了一个初始化工作的入口,其实和onCreate方法的作用一样,都是App启动的时候会调用的方法。

在这个方法中,也是限制了exported属性必须为false,grantUriPermissions属性必须为true。

  1. if (info.exported) { 
  2.     throw new SecurityException("Provider must not be exported"); 
  3. if (!info.grantUriPermissions) { 
  4.     throw new SecurityException("Provider must grant uri permissions"); 

这个初始化方法和特性,也是被很多三方库所利用,可以进行静默无感知的初始化工作,而无需单独调用三方库初始化方法。比如Facebook SDK:

  1. <provider 
  2.     android:name="com.facebook.internal.FacebookInitProvider" 
  3.     android:authorities="${applicationId}.FacebookInitProvider" 
  4.     android:exported="false" /> 
  1. public final class FacebookInitProvider extends ContentProvider { 
  2.     private static final String TAG = FacebookInitProvider.class.getSimpleName(); 
  3.  
  4.     @Override 
  5.     @SuppressWarnings("deprecation"
  6.     public boolean onCreate() { 
  7.         try { 
  8.             FacebookSdk.sdkInitialize(getContext()); 
  9.         } catch (Exception ex) { 
  10.             Log.i(TAG, "Failed to auto initialize the Facebook SDK", ex); 
  11.         } 
  12.         return false
  13.     } 
  14.  
  15.     //... 

这样一写,就无需单独集成FacebookSDK的初始化方法了,实现静默初始化。

而Jetpack中的App Startup也是考虑到这些三方库的需求,对三方库的初始化进行了一个合并,从而优化了多次创建ContentProvider的耗时。

拿到Content URI 该怎么使用?

很多人都知道该怎么配置FileProvider让别人(比如照相APP)来获取我们的Content URI,但是你们知道别人拿到Content URI之后又是怎么获取具体的File的呢?

其实仔细找找就能发现,在FileProvider.java中有注释说明:

  1. The client app that receives the content URI can open the file and access its contents by calling 
  2.  {@link android.content.ContentResolver#openFileDescriptor(Uri, String) ContentResolver.openFileDescriptor}  
  3.  to get a {@link ParcelFileDescriptor} 

也就是openFileDescriptor方法,拿到ParcelFileDescriptor类型数据,其实就是一个文件描述符,然后就可以读取文件流了。

  1. ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(intent.getData(), "r"); 
  2. FileReader reader = new FileReader(parcelFileDescriptor.getFileDescriptor()); 
  3. BufferedReader bufferedReader = new BufferedReader(reader); 

ContentProvider 实际应用

在平时的工作中,主要有以下以下几种情况和ContentProvider打交道比较多:

  • 和系统的一些App通信,比如获取通讯录,调用拍照等。上述的FileProvider也是属于这种情况。
  • 与自己的APP有一些交互。比如自家多应用之间,可以通过这个进行一些数据交互。
  • 三方库的初始化工作。很多三方库会利用ContentProvider自动初始化这一特性,进行一个静默无感知的初始化工作。

总结

ContentProvider作为四大组件之一,似乎并没有其他组件的存在感那么强。

但是他还是有自己的那一份职责,也就是在保证安全的情况下进行应用间通信,还可以扩展作为帮助初始化的组件。所以了解他,掌握它也是很重要的,没准以后哪个时候你就需要他了。

不要忽视任何一个知识点。

参考

https://mp.weixin.qq.com/s/kQmH2GnwW8FK-yNmWcheTA 

https://segmentfault.com/a/1190000021357383

https://blog.csdn.net/lmj623565791/article/details/72859156

本文转载自微信公众号「码上积木」,可以通过以下二维码关注。转载本文请联系码上积木公众号。

 

责任编辑:武晓燕 来源: 码上积木
相关推荐

2014-07-29 15:57:01

ContentProv

2023-11-07 11:17:25

Android数据共享

2009-12-18 11:22:34

Ruby source

2012-05-01 21:32:39

苹果

2021-01-18 07:31:52

MySQL LeetCode查询

2015-12-21 11:11:26

2021-08-02 13:05:49

浏览器HTTP前端

2015-03-26 18:52:38

2012-07-31 11:06:48

WebGL

2013-11-13 10:33:10

KitKatAndroidChromeOS

2009-07-16 08:50:53

微软未来操作系统Windows 7

2017-07-18 16:14:06

FileProvideAndroidStrictMode

2023-10-23 19:58:01

Android

2016-03-18 13:02:19

2019-07-16 11:06:09

TCP四次挥手半关闭

2013-09-26 11:05:24

云计算虚拟化

2013-10-08 11:16:55

谷歌云计算

2011-02-22 09:40:18

HashMap

2012-07-12 15:27:46

WebGL

2013-09-25 09:26:03

平台软件企业虚拟化云网络
点赞
收藏

51CTO技术栈公众号