透过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

目录
相关文章
|
canal SQL JSON
Elastic: canal数据同步到ES配置常见报错
所有报错均为博主在实操过程中遇到的错误和解决办法,如果有其他报错或者不同的解决办法,请留言告诉我 安装canal过程中遇到问题,先在本文中查询是否有相同报错,将会为你节约大量排错时间
1218 0
Elastic: canal数据同步到ES配置常见报错
|
Linux 网络安全
CentOS 7 SSH连接超时自动断开解决方案
CentOS 7 SSH连接超时自动断开解决方案
1361 0
CentOS 7 SSH连接超时自动断开解决方案
|
域名解析 缓存 网络协议
阿里云DNS常见问题之阿里的域名解析不对如何解决
阿里云DNS(Domain Name System)服务是一个高可用和可扩展的云端DNS服务,用于将域名转换为IP地址,从而让用户能够通过域名访问云端资源。以下是一些关于阿里云DNS服务的常见问题合集:
|
10月前
|
机器学习/深度学习 数据可视化 大数据
机器学习与大数据分析的结合:智能决策的新引擎
机器学习与大数据分析的结合:智能决策的新引擎
580 15
|
运维 Devops Java
DevOps 工具链:从代码到生产
【8月更文第30天】在现代软件开发中,DevOps(Development 和 Operations 的结合)已成为确保快速而可靠的软件交付的关键方法。DevOps 通过自动化流程将软件开发与 IT 运维相结合,从而实现持续集成 (CI) 和持续部署 (CD)。本文将介绍一个典型的 DevOps 工具链,并提供实际的代码示例来帮助您理解如何将这些工具集成在一起。
563 5
|
数据采集 监控 大数据
大数据中的ETL过程详解
【8月更文挑战第25天】ETL过程在大数据中扮演着至关重要的角色。通过合理设计和优化ETL过程,企业可以高效地整合和利用海量数据资源,为数据分析和决策提供坚实的基础。同时,随着技术的不断进步和发展,ETL过程也将不断演进和创新,以更好地满足企业的数据需求。
|
JSON 关系型数据库 MySQL
MySQL中GROUP_CONCAT与JSON_OBJECT、GROUP BY的巧妙结合:打造高效JSON数组汇总
MySQL中GROUP_CONCAT与JSON_OBJECT、GROUP BY的巧妙结合:打造高效JSON数组汇总
463 1
|
编解码 前端开发 图形学
采用Canvas Scaler与锚点系统实现UI自适应多屏幕分辨率
【7月更文第10天】在游戏开发或应用设计中,确保用户界面(UI)能够在不同屏幕分辨率和纵横比上保持良好显示效果是一项基本要求。Unity 引擎通过其强大的 UI 系统,特别是 Canvas Scaler 和锚点系统,为开发者提供了实现这一目标的高效工具。本文将深入探讨如何结合使用这两个功能来创建自适应UI布局,以适配广泛的设备屏幕。
776 0
|
缓存 Linux
软件包管理工具 - dnf
【1月更文挑战第14天】
375 0
|
安全 网络安全 开发工具
【Git】Git使用Gui图形化界面,Git中SSH协议,Idea集成Git(上)
【Git】Git使用Gui图形化界面,Git中SSH协议,Idea集成Git
393 0