FileProvider
在Android7.0及之后我们无法直接将一个FileUri共享给另一个程序进行使用。系统会抛出一个异常 FileUriExposedException 。官方是这样描述的:
                      The exception that is thrown when an application exposes a
                      
                       file://
                      
                      
                       Uri
                      
                      to another app.
                     
                      当一个应用程序暴漏一个
                      
                       file://
                      
                      
                       Uri
                      
                      给另一个app时就会抛出这个异常。
                     
                      This exposure is discouraged since the receiving app may not have access to the shared path. For example, the receiving app may not have requested the
                      
                       Manifest.permission.READ_EXTERNAL_STORAGE
                      
                      runtime permission, or the platform may be sharing the
                      
                       Uri
                      
                      across user profile boundaries.
                     
                      由于需要接收fileURI的应用程序可能无法访问共享的路径,因此不建议这样做。这可能是由于使用了
                      
                       Manifest.permission.READ_EXTERNAL_STORAGE
                      
                      权限导致,或者平台可以跨越用户配置边界共享Uri。
                     
PS:这个很好理解,比如说我有一个app被装在了手机上,但是没有申请READ_EXTERNAL_STORAGE权限(6.0后需要动态申请),但是我在另一个程序中请求这个app来读取这个文件是不是就会出现问题了,肯定就会出现异常了。所以说使用了内容提供程序,数据的读取是由内容提供者进行读取的,这样就要求数据提供者必须具有这个权限,也保证了数据安全。
                      Instead, apps should use
                      
                       content://
                      
                      Uris so the platform can extend temporary permission for the receiving app to access the resource.
                     
                      我们应该使用
                      
                       content://
                      
                      Uris对其进行替换,以便平台可以为需要访问特定资源的app扩展临时权限。
                     
                      This is only thrown for applications targeting
                      
                       Build.VERSION_CODES#N
                      
                      or higher. Applications targeting earlier SDK versions are allowed to share
                      
                       file://
                      
                      
                       Uri
                      
                      , but it's strongly discouraged.
                     
这个异常只会在目标版本大于等于7.0时抛出。之前的版本可以继续使用fileURI,不过不推荐这样做。
这些都是由于7.0开启了严格模式(StrictMode)造成的,官方在7.0的变更中是这么说的:
                      对于面向 Android 7.0 的应用,Android 框架执行的
                      
                       StrictMode
                      
                      API 政策禁止在您的应用外部公开
                      
                       file://
                      
                      URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现
                      
                       FileUriExposedException
                      
                      异常。
                     
FileProvider类的继承关系
java.lang.Object
       android.content.ContentProvider
            android.support.v4.content.FileProvider
                      
                       FileProvider
                      
                      is a special subclass of
                      
                       ContentProvider
                      
                      that facilitates secure sharing of files associated with an app by creating a
                      
                       content://
                      
                      
                       Uri
                      
                      for a file instead of a
                      
                       file:///
                      
                      
                       Uri
                      
                      .
                     
                      
                       FileProvider
                      
                      是
                      
                       ContentProvider
                      
                      的子类,它通过为一个文件创建
                      
                       content://
                      
                      
                       Uri
                      
                      来替换
                      
                       file:///
                      
                      
                       Uri
                      
                      ,以此来达到文件的安全共享。
                     
1、定义FileProvider
2、定义可用的文件路径
3、为定义的FileProvider添加文件路径
4、为特定文件生成ContentURI
5、授予ContentURI授予临时权限
1、定义FileProvider
由于FileProvider提供了ContentURI的生成方法,所以我们无需在代码中定义写一个它的子类。以下代码中的name属性是固定的,authorities可以自己定义,一般是包名字加上.fileprovider。exported设置为false,因为通常是拒绝外部直接访问的。grantUriPermissions需要为true,需要授予临时的Uri权限。
<manifest>
    <application>
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.mydomain.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
        </provider>
    </application>
</manifest>2、定义可用的文件路径
                      
                       FileProvider
                      
                      只能为预先指定的目录中的文件生成可用的ContentURI。要指定目录,需要使用
                      
                       <paths>
                      
                     
该文件需要建立在res目录下名为xml的目录下,xml目录需要自己建立。
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!--定义APP的存放目录-->
    <external-path
        name="AppInstaller"
        path="/Download"></external-path>
</paths>
                      
                       paths
                      
                      下可以包含一个或者多个子节点。
                     
<root-path/> 代表设备的根目录new File("/");//很少用
//app内部存储
<files-path/> 代表context.getFilesDir()
<cache-path/> 代表context.getCacheDir()
//sd卡存储
<external-path/> 代表Environment.getExternalStorageDirectory()
<external-files-path>代表context.getExternalFilesDirs()
<external-cache-path>代表getExternalCacheDirs()
                      我们还可以在path中用
                      
                       .
                      
                      代替所有目录。
                     
3、为定义的FileProvider添加文件路径
这里我们加入刚才添加的path文件,注意meta-data中的name项必须是android.support.FILE_PROVIDER_PATHS。
<provider
    android:name="android.support.v4.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/my_path"></meta-data>
</provider>记不住这个name怎么办?好上头!!!!懒人总是有办法。在FileProvider类的内部正好有一个定义可供我们Copy。
private static final String META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS";4、为特定文件生成ContentURI
FileProvider提供了getUriForFile函数帮助我们生成ContentURI。这里需要注意的是我们使用的文件路径必须是前边在path中定义的。否则要path何用....。
第一个参数为context,第二个是定义的provider中设置的authorities,第三个是一个File对象。
//文件路径
File file = 
    new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getCanonicalPath()
        + "/apps/MyApp.apk");
//获取文件对应的content类型Uri
Uri uri = FileProvider.getUriForFile(this, "com.mydomain.fileprovider", file);观察我们生成的Uri示例,上边是我们普通的fileUri下边是我们生成的ContentUri,区别就在于ContentUri没有暴露具体的文件路径。
//普通的fileUri(通过Uri.fromFile(file)获取)
file:///storage/emulated/0/Download/apps/MyApp.apk
//contentUri
content://com.qylost.fileproviderdemo.fileprovider/AppInstaller/MyApp.apk常见使用场景
1、跨程序共享文件
以下我们通过两个app演示两个程序使用FileProvider共享数据。提供数据的被称为:ServerApp,接受数据的被称为:ClientApp。
ServerApp:
主要是如上所说的在Manfiest中定义provider,以及定义共享路径。
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.qylost.fileproviderdemo.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/my_path"></meta-data>
</provider><paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path
        name="ShareToMyApp"
        path="."></files-path>
</paths>ClientApp:
这里我们新增了一个Main2Activity,在这里读取ServerApp通过FileProvider传来的数据。
public class Main2Activity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        Intent intent = getIntent();
        if (intent != null && intent.getData() != null) {
            try {
                ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(intent.getData(), "r");
                FileReader reader = new FileReader(parcelFileDescriptor.getFileDescriptor());
                BufferedReader bufferedReader = new BufferedReader(reader);
                String res = new Scanner(bufferedReader).useDelimiter("\\A").next();//解析传来的数据
                Toast.makeText(this, res, Toast.LENGTH_SHORT).show();//弹出
            } catch (FileNotFoundException e) {
                e.printStackTrace();
这里加入intent-filter,定义了action的名称,以及mimeType,这个在请求的时候需要用到。注意category不可少。
<activity android:name=".Main2Activity">
    <intent-filter>
        <data android:mimeType="share/text" />
        <action android:name="com.qylost.fileproviderdatareceverdemo.SHARE"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>
在ServerApp中调用如下代码,共享数据:
//在files目录下写入测试数据
writeTestData();//这里在内部files文件目录下写入了文本内容Hello File Provider!文件名为:FileProviderTest.txt
//开始共享数据
File file = new File(getFilesDir(), "FileProviderTest.txt");
Uri uri = FileProvider.getUriForFile(this, "com.qylost.fileproviderdemo.fileprovider", file);
Intent intent = new Intent("com.qylost.fileproviderdatareceverdemo.SHARE");//这个就是在上边配置intent-filter时设置的action name
intent.setDataAndType(uri, "share/text");//在上边intent-filter中设置的mimeType
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//授予临时读取权限
startActivity(intent);
2、打开App安装程序
//文件路径
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getCanonicalPath() + "/MyApp.apk");
Intent intent = new Intent(Intent.ACTION_VIEW);
//获取文件对应的content类型Uri
Uri uri = FileProvider.getUriForFile(this, "com.qylost.fileproviderdemo.fileprovider", file);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
//intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//可以不加
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
//定义文件名称
String fileName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".jpg";
String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getCanonicalPath() 
    + "/" + fileName;
//获取文件的ContentURI
File file = new File(path);
Uri uri = FileProvider.getUriForFile(this, "com.qylost.fileproviderdemo.fileprovider", file);
//定义Intent对象
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);//设置Action为MediaStore下的ACTION_IMAGE_CAPTURE
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);//设置Extra标志为输出类型
intent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);//授予临时权限
startActivityForResult(intent, 1);
//接收拍照结果
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    //拍照成功(这里可以将请求拍照的File对象定义为成员变量,这样成功后就可以拿到图片了)
    if (requestCode == 1 && resultCode == RESULT_OK) {
        Toast.makeText(this, "Success", Toast.LENGTH_SHORT).show();
    super.onActivityResult(requestCode, resultCode, data);
基本工作原理
使用fileUri的工作流程图:
1、A共享文件绝对路径给B
2、B通过路径读取数据
通过fileUri共享文件简单粗暴,直接将路径进行共享,这样做会存在一些问题:
1、文件路径暴露。
2、这个文件路径可能是一个外部存储路径(外部存储路径需要申请权限,可能App B没有这个权限,就会出现异常。再或者AppA没有外部存储读写权限,那么将文件读取交给了一个具有外部存储读写权限的App就会存在安全隐患)。
为了解决这两个问题,所以使用contentURI,使用“相对“路径解决路径暴露问题,数据读取是交由提供者来完成的。
使用ContentUri的工作流程图:
A仅仅给B分享了ContentURI,具体的文件读取是由内容/数据提供方(App A)来完成的,App B只能去问App A拿数据。
1、A共享ContentURI给B
2、B拿着这个URI找A要数据
3、A读取文件中的数据给B
手动关闭严格模式
不推荐这么来搞,不过还是要知道的。
//手动关闭严格模式
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
builder.detectAll();
StrictMode.setVmPolicy(builder.build());


