透过FileProvider再看ContentProvider

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

前言


大家应该都熟悉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诞生了。


具体做法就是:


<!-- 配置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: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的主要源码:


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


  • 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这么配置的:


<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的mineTypeContent URI 对应的ContentProvider的getType是否相同,相同情况下才能正常打开Activity。


初始化


最后再看看onCreate方法。


在APP启动流程中,自动执行所有ContentProviderattachInfo方法,并最后调用到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打交道比较多:


  • 和系统的一些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

目录
相关文章
|
8月前
|
API 数据库 Android开发
Android ContentProvider内容提供者详解
Android ContentProvider内容提供者详解
37 2
|
10月前
|
存储 API 数据库
Android:四大组件之 ContentProvider(外共享数据)
数据库在 Android 当中是私有的,不能将数据库设为 WORLD_READABLE,每个数据库都只能允许创建它的包访问。这意味着只有创建这个数据库的应用程序才可访问它。也就是说不能跨越进程和包的边界,直接访问别的应用程序的数据库。那么如何在应用程序间交换数据呢? 如果需要在进程间传递数据,可以使用 ContentProvider 来实现。
211 0
Android:四大组件之 ContentProvider(外共享数据)
|
11月前
|
API 数据库 数据库管理
ContentProvider初探
ContentProvider初探
43 0
|
API Android开发 对象存储
Android | 使用 ContentProvider 无侵入获取 Context
Android | 使用 ContentProvider 无侵入获取 Context
347 0
Android | 使用 ContentProvider 无侵入获取 Context
|
数据库
ContentProvider
构建content URI public class TaskContract { /* COMPLETED (1) Add content provider constants to the Contract Clients need to know how to access the task data, and it's your job to provide these content URI's for the path to that data: 1) Content authority, 2) Base content
111 0
ContentProvider
|
XML 缓存 API
Android 7.0之访问文件的权限和FileProvider类
转载请标明出处: http://blog.csdn.net/djy1992/article/details/72533310 本文出自:【奥特曼超人的博客】 权限更改 Android 7.0 做了一些权限更改,这些更改可能会影响您的应用。
3586 0
|
存储 Android开发
【错误记录】Android 文件分享 FileProvider 设置错误
【错误记录】Android 文件分享 FileProvider 设置错误
175 0
【错误记录】Android 文件分享 FileProvider 设置错误