美文网首页Android Classandroid开发Android
Android 7.0 FileUriExposedExcept

Android 7.0 FileUriExposedExcept

作者: hqzxzwb | 来源:发表于2016-12-13 02:02 被阅读10425次

    从Android 7.0开始,一个应用提供自身文件给其它应用使用时,如果给出一个file://格式的URI的话,应用会抛出FileUriExposedException。这是由于谷歌认为目标app可能不具有文件权限,会造成潜在的问题。所以让这一行为快速失败。详见这里。这里讨论两种解决方式。

    1 FileProvider方式

    这是谷歌官方推荐的解决方案。即使用FileProvider来生成一个content://格式的URI。具体实现方式如下:

    1. manifest声明
      在manifest中声明一个provider。name(即类名)为android.support.v4.content.FileProvider。
    <manifest>
        ... 
        <application>
            ...
            <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/file_paths" />
            </provider>
            ...
        </application>
    </manifest>
    

    其中authorities可以自定义。为了避免和其它app冲突,最好带上自己app的包名。file_paths.xml中编写该Provider对外提供文件的目录。文件放置在res/xml/下。
    2.编写file_paths.xml
    文件格式如下:

    <paths xmlns:android="http://schemas.android.com/apk/res/android">
        <files-path name="my_images" path="images/"/>
        ...
    </paths>
    

    内部的element可以是files-pathcache-pathexternal-pathexternal-files-pathexternal-cache-path,分别对应Context.getFilesDir(),Context.getCacheDir(),Environment.getExternalStorageDirectory(),Context.getExternalFilesDir(),Context.getExternalCacheDir()等几个方法。后来翻看源码发现还有一个没有写进文档的,但是也可以使用的element,是root-path,直接对应文件系统根目录。不过既然没有写进文档中,其实还是有将来移除的可能的。使用的话需要注意一下风险。

    3.在Java代码当中使用
    以分享一个图片为例:

    File file = ...;    //要分享的图片文件
    Uri uri = FileProvider.getUriForFile(context, "com.mydomain.fileprovider", file);    //第二个参数是manifest中定义的`authorities`
    Intent intent = new Intent(Intent.ACTION_SEND);
    intent.setType("image/*");
    intent.putExtra(Intent.EXTRA_TITLE, title);
    intent.putExtra(Intent.EXTRA_TEXT, text);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);    //这一步很重要。给目标应用一个临时的授权。
    startActivity(intent);    //或者其它最终处理方式
    

    2 VmPolicy方式

    以上方法固然是推荐使用的,正确的方法。但是我在实际开发中遇到这样的问题。某些应用(此处点名新浪微博)根本无法理解一个指向文件的content://格式的URI。新浪微博接收到这类URI之后,无法加载图片,并会在点击发送微博时崩溃。
    另一方面,新浪微博对权限管理的处理采取了一种比较流氓的方式。它会在启动时申请文件读写权限,而如果拒绝该权限的话,居然就直接退出了。我反正是不信什么需要文件权限来放缓存放数据的说辞。放缓存放数据有着一堆不需要权限的目录可用。但是这样一来,我们其实是不需要担心传递一个file://格式URI过去而对方没有权限的。
    话说回来,如何解决这一问题呢?我在调研的时候观察到严格模式的一个方法:StrictMode.VmPolicy.Builder.detectFileUriExposure()。顾名思义,调用这个方法就会检测FileUriExposure这件事。这个方法其实从API18就有了,是不是有可能在API24变成了默认选项呢?
    在Application.onCreate加入如下代码,置入一个不设防的VmPolicy:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
        StrictMode.setVmPolicy(builder.build());
    }
    

    再用旧的方式直接把file://格式的URI发送出去。居然成功了,没有再抛出FileUriExposedException。

    3 小结

    最终我采取的综合方案是,先使用PackageManager.checkPermission检测对方的app有没有取得文件读写权限。如果有的话,给对方发送file://格式URI。如果没有的话,给对方发送FileProvider生成的URI并临时授权。
    原本一个有标准解决方案的问题,因为某些应用不遵循规范而需要做更多的调研和workaround。实在是麻烦。希望可以帮助到遇到同样问题的人。

    相关文章

      网友评论

      • xuguilu:Uri uri = FileProvider.getUriForFile(context, "com.mydomain.fileprovider", file);
        想请教一下,如果传入的是uri不是file该肿么办?
      • 醉酒肆之:我第一种设置失败。方法:app依赖library,相机方法位于library中,我在library中AndroidManifest.xml 中添加provider,res中添加xml,相机方法中添加代码(library包名),错报空指针。
        随后更换在app中AndroidManifest.xml 中添加provider,res中添加xml,相机方法中添加代码(app包名),报错空指针。
        最后无奈使用,第二种使用VmPolicy方式,可以正常拍照。但是疑惑网上很多说sdk>=24,在onCreate中还需要添加这段API18的代码:builder.detectFileUriExposure()。而你不添加为什么也可以呢?
        下面附上相机部分代码:
        Intent iParam = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        iParam.addCategory(Intent.CATEGORY_DEFAULT);
        File modelOfflineImagePathFolder = new File(strModelOfflineImagePath);
        if (!modelOfflineImagePathFolder.exists()) modelOfflineImagePathFolder.mkdirs();
        File imageFile = new File(strPhotographImagePath);
        if (imageFile.exists()) imageFile.delete();
        // Uri uriImage = Uri.fromFile(imageFile);//api<24
        Uri uriImage = FileProvider.getUriForFile(
        fragment.getContext(),fragment.getActivity().getPackageName() + ".provider",
        imageFile);
        iParam.putExtra(MediaStore.EXTRA_OUTPUT, uriImage);
        if (fragment.isAdded()) fragment.startActivityForResult(iParam, requestCode);
        return iParam;
      • clank520: Uri URI = FileProvider.getUriForFile(context, "cn.valueonline.capital.fileprovider", file);
        Intent intent = new Intent();
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //添加这一句表示对目标应用临时授权该Uri所代表的文件
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        //设置intent的Action属性
        intent.setAction(Intent.ACTION_VIEW);
        //获取文件file的MIME类型
        String type = getMIMEType(file);
        //设置intent的data和Type属性。
        intent.setDataAndType(/*uri*/URI, type);
        //跳转
        context.startActivity(intent); 这是我的代码 可以帮忙看下吗
        clank520:<?xml version="1.0" encoding="utf-8"?>
        <resources>
        <paths>
        <external-path name="camera_photos" path=""/>
        <external-path name="external_files" path=""/>
        </paths>
        </resources>
        clank520: <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="cn.valueonline.capital.fileprovider"
        android:grantUriPermissions="true"
        android:exported="false">
        <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
        </provider>
        clank520:临时授权URI是这个 content://cn.valueonline.capital.fileprovider/external_files/Android/data/cn.valueonline.capital/cache/3e1dd0da-26b6-4200-b331-d9f8aa302144.docx
      • clank520:你好 我看了下你写的东西, 解决了我一部分东西, 打算还有个问题 我打开.docx的时候报这个ActivityNotFoundException: No Activity found to handle Intent 异常没办法打开这个文件,但是pdf文件可以
      • b5679b62ce49:回过头来看代码,突然又有个问题,如楼主清楚 还望解答:7.0系统 怎么通过content格式找到具体存储路径的啊? 打出来文件uri格式是:content://com.criminalintent.fileprovider/my_images/Android/data/...
        hqzxzwb:@jkdev 其实就是不给对方真正的文件路径,而是虚拟一个URI并提供了临时授权,相当于一个临时虚拟的路径。
        b5679b62ce49:@hqzxzwb 噢噢,好吧 我就是想弄清楚下,有点不能理解 多谢啦
        hqzxzwb:@jkdev 不应当去获取文件具体路径。如果需要读取文件的话,使用context.getContentResolver().openInputStream(uri)来获取一个InputStream。
      • b5679b62ce49:你好,为什么我的还是不行啊 配了provider, xml的path也写了
        b5679b62ce49:@hqzxzwb 对6.0的运行时权限还是熟悉不够
        b5679b62ce49:@hqzxzwb 多谢 ,已经解决 搞了好久:pray:
        hqzxzwb:可不可以贴一下代码呢
      • cdfd8e7a3fbe:好文章为什么没人发现呢?

      本文标题:Android 7.0 FileUriExposedExcept

      本文链接:https://www.haomeiwen.com/subject/zbyvmttx.html