Android爬坑之旅之FileProvider(Failed to find configured root that contains)

来源:互联网 发布:软件接口设计说明书 编辑:程序博客网 时间:2024/06/04 22:04

最近在测试FileProvider相关功能的时候,在从自定义相册选择图片通过FileProvider来获取content uri的时候程序突然崩溃了,报出了

Failed to find configured root that contains xxxx

的错误,一开始以为是自己的配置出错了,但是参照官方文档改来改去仍然没有任何作用,通过绞尽脑汁地排查,终于发现了错误原因,并找到了正确的解决方案,在了解最终的解决方案之前我们先对FileProvider做个简单的了解和回顾。


FileProvider简介

很久之前就知道FileProvider了,然而之前做过的实际项目中却很少用到,一方面自己的项目没有涉及到相关的场景,一方面也是自己对文件安全没有太在意,虽然看官方文档的时候多次读过,却从来没想到去使用它。

但随着Android 7.0的到来,为了进一步提高私有文件的安全性,Android不再由开发者放宽私有文件的访问权限,之前我们一直使用”file:///”绝对路径来传递文件地址的方式,在接收方访问时很容易触发SecurityException的异常。

因此,为了更好的适配Android 7.0,例如相机拍照这类涉及到文件地址传递的地方就用上了FileProvider,FileProvider也更好地进入了大家的视野。

其实FileProvider是ContentProvider的一个特殊子类,本质上还是基于ContentProvider的实现,FileProvider会把”file:///”的路径转换为特定的”content://”形式的content uri,接收方通过这个uri再使用ContentResolver去媒体库查询解析。


FileProvider使用方法:

  1. 在AndroidManifest.xml里声明Provider
<manifes xmlns:android="http://schemas.android.com/apk/res/android"    package="com.example.myapp">       <application                  ...>            <provider                   android:name="android.support.v4.content.FileProvider" //指向v4包里的FileProvider类              android:authorities="com.example.myapp.fileprovider" //对应你的content uri的基础域名,生成的uri将以content://com.example.myapp.fileprovider作为开头              android:grantUriPermissions="true" //设置允许获取访问文件的临时权限              android:exported="false"//设置不允许导出,我们的FileProvider应该是私有的          >                      <meta-data              android:name="android.support.FILE_PROVIDER_PATHS"               android:resource="@xml/filepaths" //用于设置FileProvider的文件访问路径           />                  </provider>                  ...          </application></manifest>

2.配置FileProvider文件共享的路径

接下来在我们的res目录下创建一个xml目录,在xml目录下新建一个filepaths.xml,这个xml的名字可以根据项目的具体情况来取,对应第一步中mainifest配置里的FileProvider路径的配置中指定的文件

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

在标签中我们必须配置至少一个或多个path子元素,
path子元素则用来定义content uri所对应的路径目录。
这里以为例:
* name属性:指明了FileProvider在content uri中需要添加的部分,这里的name为my_images,所以对应的content uri为

content://com.example.myapp.fileprovider/my_images
  • path属性:标签对应的路径地址为Context.getFilesDir()]()返回的路径地址,而path属性的值则是该路径的子路径,这里的path值为”images/”,那组合起来的路径如下所示:
Content.getFilesDir() + "/images/"
  • name属性跟path属性一一对应,根据上面的配置,当访问到文件
content://com.example.myapp.fileprovider/my_images/xxx.jpg

就会找到这里配置的path路径

Content.getFilesDir() + "/images/"

并查找xxx.jpg文件

对应路径的配置,官方文档列出了如下配置项:

<files-path name="*name*" path="*path*" />

对应 Context.getFilesDir() 的路径地址

<cache-path name="*name*" path="*path*" />

对应 getCacheDir() 获取的路径

<external-path name="*name*" path="*path*" />

对应 Environment.getExternalStorageDirectory()的路径地址

<external-files-path name="*name*" path="*path*" />

对应Context#getExternalFilesDir(String) Context.getExternalFilesDir(null) 返回的路径地址

<external-cache-path name="*name*" path="*path*" />

以此类推,我们可以根据自己所需共享的目录来配置不同的path路径.

3.配置完共享地址后,获取content uri的值

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

4.授予一个uri的临时权限,并将值传给接收方app
我们假设接收方app使用startActivityForResult来请求app的图片资源,
则请求方获取请求后根据上面的代码获取

Intent intent = new Intent()intent.setDataAndType(fileUri,getContentResolver().getType(fileUri));intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);setResult(intent);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

用来授予接收方对于文件操作的临时权限,可以设置为 FLAG_GRANT_READ_URI_PERMISSION
FLAG_GRANT_WRITE_URI_PERMISSION
或者两者都允许

5.接收方在onActivityResult里根据获取的content uri来查询数据

Uri returnUri = returnIntent.getData();    Cursor returnCursor =  getContentResolver().query(returnUri, null, null, null, null);    int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);   int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);returnCursor.moveToFirst();    TextView nameView = (TextView) findViewById(R.id.filename_text); TextView sizeView = (TextView) findViewById(R.id.filesize_text); nameView.setText(returnCursor.getString(nameIndex)); sizeView.setText(Long.toString(returnCursor.getLong(sizeIndex)));

这下我们FileProvider的整个使用流程就完整了


FileProvider的坑(Failed to find configured root that contains)

failed to find configured root that contains

经过对FileProvider的进一步了解和巩固,终于到了揭开错误原因的时候了

原来手上这台红米手机的相册选择了存储位置在外置SD卡上,因此部分相册照片的存放地址实际上是在外置sd卡上的。
下面是我的path配置

<paths xmlns:android="http://schemas.android.com/apk/res/android>    <external-path name="external_files" path="."/></paths>

乍一看貌似没什么问题,path设置的是external的根路径,对应Environment.getExternalStorageDirectory(),
然而这个方法所获取的只是内置SD卡的路径,所以当选择的相册中的图片是外置SD卡的时候,就查找不到图片地址了,因此便抛出了failed to find configured root that contains的错误。

那怎么解决呢,官方文档给出的配置项并没有能够对应外部地址的,那我们只有从FileProvider的源码入手看看有没有什么办法了.

因为FileProvider是通过path的配置来限制路径范围的,因此我们可以通过path的解析作为入口点来进行分析,
于是乎便找到了parsePathStrategy()这个方法,
它就是专门用来解析我们的path路径的xml文件的.

private static final String TAG_ROOT_PATH = "root-path";private static final String TAG_FILES_PATH = "files-path";private static final String TAG_CACHE_PATH = "cache-path";private static final String TAG_EXTERNAL = "external-path";private static final File DEVICE_ROOT = new File("/");private static PathStrategy parsePathStrategy(Context context, String authority)                  throws IOException, XmlPullParserException {           ...      while ((type = in.next()) != END_DOCUMENT) {                  if (type == START_TAG) {                          //<external-path>等根标签              final String tag = in.getName();              //标签中的name属性              final String name = in.getAttributeValue(null, ATTR_NAME);              //标签中的path属性                String path = in.getAttributeValue(null, ATTR_PATH);                          File target = null;                          //标签对比              if (TAG_ROOT_PATH.equals(tag)) {                                  target = buildPath(DEVICE_ROOT, path);                          } else if (TAG_FILES_PATH.equals(tag)) {                  target = buildPath(context.getFilesDir(), path);                          } else if (TAG_CACHE_PATH.equals(tag)) {                                  target = buildPath(context.getCacheDir(), path);                          } else if (TAG_EXTERNAL.equals(tag)) {                                  target = buildPath(Environment.getExternalStorageDirectory(), path);                        }                      if (target != null) {                              strat.addRoot(name, target);                      }            }    }    return strat;}private static File buildPath(File base, String... segments) {        File cur = base;    //根文件    for (String segment : segments) {                if (segment != null) {                        //创建以cur为根文件,segment为子目录的文件            cur = new File(cur, segment);               }        }        return cur;}

这里我把一些关键代码摘抄了出来,通过这些关键代码我们可以看到,
在xml解析到对应的标签后,会执行 buildPath() 方法来将根标签(等)对应的路径作为文件根路径,
同时将标签中的path属性所指定的路径作为子路径创建对应的File对象,最终生成了一个cur文件对象来作为目标文件,从而限制FileProvider的文件访问路径位于cur的path路径下。

其中一段代码让我发现了一丝异样

 if (TAG_ROOT_PATH.equals(tag)) {                        target = buildPath(DEVICE_ROOT, path);             }

TAG_ROOT_PATH对应了常量”root-path”,
而DEVICE_ROOT则对应了new File(“/”);
没错,虽然官方文档没有写出来,但是代码里竟然留了个的根标签,而它的路径对应的是DEVICE_ROOT指向的整个存储的根路径。

于是根据代码的逻辑,将我们的路径的配置文件做了调整,
改成了

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

这时运行程序,会发现FileProvider已经不会报错了,因为文件的搜索路径已经变成了我们客户端的整个根路径,就这样,我们的问题终于得到了顺利地解决!

通过对FileProvider的源码分析,正如开头所说的,FileProvider继承自ContentProvider,因此还有一种方法就是继承ContentProvider然后自定义Provider来处理,不过FileProvider本身的代码写得已经很完善了,所以一般没有这个必要,这里我给出了一段简易代码仅做参考:
同样在AndroidManifest里配置我们的provider

<provider          android:name="org.test.img.MyProvider"//指向自定义的Provider      android:authorities="com.test.img" />

然后自定义一个MyProvider

public class MyProvider extends ContentProvider {        private String mAuthority;    @Override    public void attachInfo(Context context, 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");            }            mAuthority = info.authority;    }    @Override        public ParcelFileDescriptor openFile(Uri uri, String mode)  throws FileNotFoundException {              File file = new File(uri.getPath());              ParcelFileDescriptor parcel = ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY);              return parcel;        }        @Override        public boolean onCreate() {              return true;        }       @Override        public int delete(Uri uri, String s, String[] as) {              throw new UnsupportedOperationException("Not supported by this provider");        }        @Override        public String getType(Uri uri) {                  throw new UnsupportedOperationException("Not supported by this provider");        }    @Override            public Uri insert(Uri uri, ContentValues contentvalues) {                  throw new UnsupportedOperationException("Not supported by this provider");        }        @Override        public Cursor query(Uri uri, String[] as, String s, String[] as1, String s1) {        throw new UnsupportedOperationException("Not supported by this provider");        }        @Override        public int update(Uri uri, ContentValues contentvalues, String s, String[] as) {                throw new UnsupportedOperationException("Not supported by this provider");        }    public static Uri getUriForFile(File file) {            return new Uri.Builder().scheme("content").authority(mAuthority).encodedPath(file.getPath).build();    }}

这样我们的代码就可以通过自定义的getUriForFile来获取content uri了

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 身份号码泄露了怎么办 我身份证泄露了怎么办 身份证信息泄漏了怎么办 无锡身份证丢了怎么办 人在外地怎么办身份证 欠空放公司不还怎么办 兼职要身份证照片怎么办 身份证刷不了磁怎么办 身份证不能刷了怎么办 身份证指纹错了怎么办 指纹手机丢了怎么办 异地办理临时身份证怎么办 杭州办理外地身份证怎么办 办理身份证没有户口本怎么办 2018身份证掉了怎么办 双户口注销社保怎么办 常用户口被注销怎么办 刚到厦门怎么办身份证 新疆身份证丢了怎么办 技能证书丢了怎么办 16岁拍身份证怎么办 16岁以下怎么办身份证 办身份证要证明怎么办 15岁怎么办银行卡淘宝 满16岁怎么办银行卡 电脑最小化后不见了怎么办 满16周岁怎么办银行卡 给儿童办身份证怎么办 当兵前身份证号码不符怎么办 身份证后六位密码x怎么办 借了贷款人死了怎么办 贷款实在还不起怎么办 个人贷款还不起怎么办 珠海派出所办事不公平怎么办 改身份证后学籍怎么办? 威海身份证丢了怎么办 车牌轻微变形了怎么办 车牌照丢一个怎么办 小车车牌掉了怎么办 车牌撞变形了怎么办 车牌烂了一块怎么办