一、前言

    从Android N(即Android 7.0)开始,Android系统开始限制向其他应用发起file:///开头的URI,因为要是允许传递file:///开头的Uri,那么被你应用启动的其他应用就能够自由地去操作这个文件,但是仔细想想,并不应该是这样,假如要分享操作文件是你应用的私有文件呢?而且通过file:///传递文件路径的话,对于文件访问权限的控制也比较麻烦,需要去修改这个文件的访问权限。因此,其他应用访问文件需要通过你的应用来分配权限去操作才是最正确的做法,而FileProvider就能达到这个目的
官方说明

二、异常现象

1、应用的targetSdkVersion设置大于等于24时,发起带有file://开头的Uri的Intent,比如:拍照

private File createImageFile() throws IOException {
        // Create an image file name
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        String imageFileName = "JPEG_" + timeStamp + "_";
        File storageDir = new File(Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_DCIM), "Camera");
        File image = File.createTempFile(
                imageFileName,  /* prefix */
                ".jpg",         /* suffix */
                storageDir      /* directory */
        );

        // Save a file: path for use with ACTION_VIEW intents
        mCurrentPhotoPath = "file:" + image.getAbsolutePath();
        return image;
    }

private void dispatchTakePictureIntent() throws IOException {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        // Ensure that there's a camera activity to handle the intent
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            // Create the File where the photo should go
            File photoFile = null;
            try {
                photoFile = createImageFile();
            } catch (IOException ex) {
                // Error occurred while creating the File
                return;
            }
            // Continue only if the File was successfully created
            if (photoFile != null) {
                Uri photoURI = Uri.fromFile(createImageFile());
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
                startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
            }
        }

2、你会发现,应用闪退,然后异常日志类似如下:android.os.FileUriExposedException

android.os.FileUriExposedException: file:///storage/emulated/0/sswlPic/Barcodes/1574066862876.png exposed beyond app through ClipData.Item.getUri()
        at android.os.StrictMode.onFileUriExposed(StrictMode.java:1978)
        at android.net.Uri.checkFileUriExposed(Uri.java:2371)
        at android.content.ClipData.prepareToLeaveProcess(ClipData.java:963)
        at android.content.Intent.prepareToLeaveProcess(Intent.java:10343)
        at android.content.Intent.prepareToLeaveProcess(Intent.java:10349)
        at android.content.Intent.prepareToLeaveProcess(Intent.java:10328)
        at android.app.Instrumentation.execStartActivity(Instrumentation.java:1669)
        at android.app.Activity.startActivityForResult(Activity.java:4731)
        at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:767)
        at android.support.v4.app.ActivityCompat.startActivityForResult(ActivityCompat.java:234)
        at android.support.v4.app.FragmentActivity.startActivityFromFragment(FragmentActivity.java:881)
        at android.support.v4.app.FragmentActivity$HostCallbacks.onStartActivityFromFragment(FragmentActivity.java:995)
        at android.support.v4.app.Fragment.startActivity(Fragment.java:1084)
        at android.support.v4.app.Fragment.startActivity(Fragment.java:1073)
        at com.sswl.tool.fragment.WifiServerFragment.share(WifiServerFragment.java:316)
        at com.sswl.tool.fragment.WifiServerFragment.access$100(WifiServerFragment.java:43)
        at com.sswl.tool.fragment.WifiServerFragment$1.onLongClick(WifiServerFragment.java:60)
        at android.view.View.performLongClickInternal(View.java:6783)
        at android.view.View.performLongClick(View.java:6741)
        at android.view.View.performLongClick(View.java:6759)
        at android.view.View$CheckForLongPress.run(View.java:26246)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:232)
        at android.app.ActivityThread.main(ActivityThread.java:7225)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:500)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:908)

三、异常分析

    从Android 7.0(Api为24)开始,Android系统禁止向其他应用传递file:///开头的Uri,否则会抛出android.os.FileUriExposedException异常,需要借助FileProvider将file:///转为content://开头的Uri

四、解决方案

1、为了避免跟第三方库的FileProvider出现冲突,首先,我们先简单创建一个类继承FileProvider:

public class GenericFileProvider extends FileProvider {}

2、在res/xml目录下创建一个名字为:provider_paths.xml的文字(名字随意),内容类似如下:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    tools:ignore="ResourceName">
    <!--代表的目录即为:Environment.getExternalStorageDirectory()/Android/data/包名/files-->
    <files-path name="files-path" path="/." />
    <files-path name="my_images" path="images/"/>
    <files-path name="my_docs" path="docs/"/>
    <cache-path name="cache-path" path="/." />
    <external-path name="external-path" path="/." />
    <external-files-path name="external-files-path" path="/." />
    <external-cache-path name="external-cache-path" path="/." />
</paths>

【备注】
<paths> 标签元素里面需要包括以下一个或者多个元素:
<files-path name="name" path="path" />代表的是:你app内部存储的 files/ 子目录.,等同于Context.getFilesDir()返回的值
<cache-path name="name" path="path" /> 代表的是:你app内部存储的 cache/ 子目录.,等同于Context.getCacheDir()返回的值
<external-path name="name" path="path" />代表的是:外部存储的根目录.,等同于Environment.getExternalStorageDirectory()返回的值
<external-files-path name="name" path="path" />代表的是:你app外部存储的根目录.,等同于 Context#getExternalFilesDir(String) 或Context.getExternalFilesDir(null).返回的值
<external-cache-path name="name" path="path" />代表的是:你app外部缓存的根目录.,等同于 Context.getExternalCacheDir()返回的值
<external-media-path name="name" path="path" />代表的是:你app外部媒体的根目录.,等同于 Context.getExternalMediaDirs()返回的值

替换规则是:将path代表的实际目录替为name代表的虚拟目录 例如,下面这段代码:

File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);

最后生成的uri是:content://packagename.fileprovider/my_images/default_image.jpg,这里是实际路径images被替换为了虚拟目录my_images
3、在AndroidManifest.xml的application标签下增加一个provider子标签,为了避免冲突,android:authorities设置值为${applicationId}.provider

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    <application
        ...
        <provider
            android:name=".GenericFileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>
    </application>
</manifest>

4、然后将之前的代码:

Uri photoURI = Uri.fromFile(createImageFile());

改写为:

Uri photoURI = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", createImageFile());

5、接收这个Intent的app可以通过 ContentResolver.openFileDescriptor 获取 ParcelFileDescriptor对象来访问这个文件

【注意】: 你可以通过设置 FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION 来临时授权给app读写,intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);可以通过调用revokeUriPermission()来取消授权,否则要下次重启才取消授权