Android FileProvider使用

使用FileProvider出现如下错误

java.lang.IllegalArgumentException: Failed to find configured root that contains

原文链接:https://blog.csdn.net/u013553529/article/details/83900704#how

FileProvider 路径配置策略的理解

★ FileProvider的使用

在AndroidManifest.xml中

<provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="set_your_package_name"
        android:exported="false"
        android:grantUriPermissions="true">
    <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/filepath_data" />
</provider>

通常设置android:exported="false",以保证权限最小化。
android:resource="@xml/filepath_data"中,filepath_data.xml文件是配置哪些路径是可以通过FileProvider访问的。
meta-data是以键值对的方式保存(key-value pairs)。android.support.FILE_PROVIDER_PATHS作为meta-data的键(key),@xml/filepath_data作为meta-data的值(value)。在FileProvider中会读取meta-data中的android.support.FILE_PROVIDER_PATHS对应的值。

filepath_data.xml

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="my_files" path="tempfiles" />
    <external-path name="my_external" path="Download"/>
    <cache-path name="my_cache" />
</paths>

files-path对应app的/data/data/<package_name>/files/目录,path="tempfiles"是指子目录,即完整的目录为/data/data/<package_name>/files/tempfiles。

external-path对应的是内置的sdcard目录/sdcard/,path="Download"是子目录,完整目录为 /sdcard/Download。

cache-path对应的是/data/data/<package_name>/cache/,这个例子里没有子目录。

name属性相当于这些路径的别名,通过name可以获取到相对应的路径。

★ 如何更好地理解这几个路径的用法?

通过学习Android中解析filepath_data.xml文件的源代码,可以更容易理解和掌握这些路径的具体含义。
代码请参考FileProvider的parsePathStrategy()方法。如果想了解如何执行到此方法的,可以参考Android ContentProvider的加载过程

parsePathStrategy()方法的代码如下(省略了一些代码):

XML文件中的TAG和属性:

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 String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";

private static final String ATTR_NAME = "name";
private static final String ATTR_PATH = "path";

XML中各个tag对应的路径,如下表:

  • Tag 对应的路径
  • root-path 根目录/
  • files-path /data/user/0/<package_name>/files 或者/data/data/<package_name>/files
  • 这两个目录指向相同的位置
  • cache-path /data/user/0/<package_name>/cache 或者 /data/data/<package_name>/cache
  • external-path /storage/emulated/0或者/sdcard/
  • external-files-path /storage/emulated/0/Android/data/<package_name>/files 或者 /sdcard/Android/data/<package_name>/files
  • external-cache-path /storage/emulated/0/Android/data/<package_name>/cache 或者 /sdcard/Android/data/<package_name>/cache
parsePathStrategy() @FileProvider

private static PathStrategy parsePathStrategy(Context context, String authority)
        throws IOException, XmlPullParserException {
    final SimplePathStrategy strat = new SimplePathStrategy(authority);

    final ProviderInfo info = context.getPackageManager()
            .resolveContentProvider(authority, PackageManager.GET_META_DATA);
    // META_DATA_FILE_PROVIDER_PATHS 为"android.support.FILE_PROVIDER_PATHS", 这是在AndroidManifest.xml中所使用的。
    // 读取filepath_data.xml文件(本文中的例子)
    final XmlResourceParser in = info.loadXmlMetaData(
            context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);

    int type;
    while ((type = in.next()) != END_DOCUMENT) {
        if (type == START_TAG) {
            final String tag = in.getName();

            // 获取属性"name"和"path"
            final String name = in.getAttributeValue(null, ATTR_NAME);
            String path = in.getAttributeValue(null, ATTR_PATH);

            File target = null;
            if (TAG_ROOT_PATH.equals(tag)) {
                // "root-path"标签,DEVICE_ROOT = new File("/"),系统的根目录
                target = DEVICE_ROOT;
            } else if (TAG_FILES_PATH.equals(tag)) {
                // "files-path"标签,getFilesDir(),对应"/data/user/0/<package_name>/files"目录
                target = context.getFilesDir();
            } else if (TAG_CACHE_PATH.equals(tag)) {
                // "cache-path"标签,对应"/data/user/0/<package_name>/cache"目录
                target = context.getCacheDir();
            } else if (TAG_EXTERNAL.equals(tag)) {
                // "external-path"标签,对应内置sdcard目录,例如"/storage/emulated/0", 或者"/sdcard/"
                target = Environment.getExternalStorageDirectory();
            } else if (TAG_EXTERNAL_FILES.equals(tag)) {
                // "external-files-path"标签,对应 "/storage/emulated/0/Android/data/<package_name>/files"目录
                File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
                if (externalFilesDirs.length > 0) {
                    target = externalFilesDirs[0];
                }
            } else if (TAG_EXTERNAL_CACHE.equals(tag)) {
                // "external-cache-path"标签,对应"/storage/emulated/0/Android/data/<package_name>/cache"目录
                File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
                if (externalCacheDirs.length > 0) {
                    target = externalCacheDirs[0];
                }
            }

            if (target != null) {
                // 将路径拼起来,name作为key,完整路径是value
                strat.addRoot(name, buildPath(target, path));
            }
        }
    }
    return strat;
}

注意:/data/user/0是指向/data/data目录,所以/data/user/0/<package_name>/files也就是/data/data/<package_name>/files。
执行下面的命令可以看到:

ls -ld /data/user/0
lrwxrwxrwx 1 root root 10 2017-04-15 00:25 /data/user/0 -> /data/data

以filepath_data.xml这个文件为例,再看一下都配置了哪些路径:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="my_files" path="tempfiles" />
    <external-path name="my_external" path="Download"/>
    <cache-path name="my_cache" />
</paths>

files-path对应app的/data/data/<package_name>/files/目录,path="tempfiles"是指子目录,拼起来的完整路径为/data/data/<package_name>/files/tempfiles。

external-path对应的是内置的sdcard目录/sdcard/,path="Download"是子目录,完整目录为 /sdcard/Download。

cache-path对应的是/data/data/<package_name>/cache/,这个例子里没有子目录。

★ 如何使用filepath_data.xml中配置的路径?

◇ 通过uri来访问文件

以<files-path name="my_files" path="tempfiles" />为例。

通过Uri content://<authority>/my_files/<file_name>来访问my_files标签对应的目录中的文件<file_name>。

例如,content://my_authority/my_files/path/to/file001.txt对应的就是/data/data/<package_name>/files/path/to/file001.txt。

代码可以参考FileProvider的getFileForUri(),下面是部分主要代码。

public File getFileForUri(Uri uri) {
    String path = uri.getEncodedPath();

    final int splitIndex = path.indexOf('/', 1);
    // 解析出tag,在此例中tag是my_files
    final String tag = Uri.decode(path.substring(1, splitIndex));
    // path是uri中的<file_name>,path可以只是文件名,也可以是带路径的文件名
    path = Uri.decode(path.substring(splitIndex + 1));
    // 这个tag就是`<files-path name="my_files" path="tempfiles" />`中的属性name
    final File root = mRoots.get(tag);
    // 将路径拼起来,构成实际的文件路径
    File file = new File(root, path);
    // 略
    return file;
}

◇ 获取文件对应的Uri

参考FileProvider中的getUriForFile()
注:所有出错处理的代码都忽略了。

public Uri getUriForFile(File file) {
    String path;
    try {
        path = file.getCanonicalPath();
    } catch...

    // 这段代码是为了找到文件file最匹配的路径,即取匹配最长的那个root
    Map.Entry<String, File> mostSpecific = null;
    for (Map.Entry<String, File> root : mRoots.entrySet()) {
        final String rootPath = root.getValue().getPath();
        if (path.startsWith(rootPath) && (mostSpecific == null
                || rootPath.length() > mostSpecific.getValue().getPath().length())) {
            mostSpecific = root;
        }
    }

    final String rootPath = mostSpecific.getValue().getPath();
    // path是以/开头的
    if (rootPath.endsWith("/")) {
        // 如果rootPath以/开头,则将rootPath长度的内容去掉后,剩下的就是uri中使用的路径
        path = path.substring(rootPath.length());
    } else {
        // 如果rootPath不是以/开头,则需要去掉path的第一个/后,再去掉rootPath.length()的内容后,剩下的就是uri中使用的路径
        path = path.substring(rootPath.length() + 1);
    }

    // mostSpecific.getKey()对应的是路径配置文件中的属性name
    // 最终拼起来像这样:content://<authority>/<name>/<file_path>
    path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
    return new Uri.Builder().scheme("content")
            .authority(mAuthority).encodedPath(path).build();
}

以<external-path name="my_external" path="Download"/>为例。

对于内置sdcard中Download目录下的文件file002.txt,其路径为/sdcard/Download/file002.txt。对应的uri为content://<authority>/my_external/file002.txt。

★ Android ContentProvider的加载过程

当某个app的进程要启动时,Dalvik虚拟机先fork出一个新的进程,然后将此进程的名字命名为这个app的包名,然后通过反射的方式,执行 ActivityThread 的静态的main()方法,在main()中创建主线程 ActivityThread,并将app中的各种组件信息附加到该进程中,即调用attach()方法。

从这个attach()方法开始,来描述ContentProvider的加载过程。

说明:@ActivityThread表示代码在ActivityThread类中。

-> main() @ActivityThread
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
-> attach() @ActivityThread
-> mgr.attachApplication(mAppThread); @ActivityThread
    mgr是IActivityManager类型的接口,是 ActivityManagerProxy 的实例,最终调用 ActivityManagerService的对应的方法。
    这里会切换进程到system_server进程中(ActivityManagerService所在的进程)

-> attachApplication() @ActivityManagerService
-> attachApplicationLocked() @ActivityManagerService
    List<ProviderInfo> providers = normalMode ? generateApplicationProvidersLocked(app) : null;
    thread.bindApplication(processName, appInfo, providers, ...);
    thread 是 IApplicationThread 类型的接口,用来向app所在进程发送消息,即调用app进程中的方法。切换进程到app进程。

-> bindApplication() @ActivityThread
    AppBindData data = new AppBindData();
    data.providers = providers;
    sendMessage(H.BIND_APPLICATION, data);
-> handleMessage() @ActivityThread
    case BIND_APPLICATION:
        handleBindApplication(data);
-> handleBindApplication() @ActivityThread
        if (!data.restrictedBackupMode) {
            if (!ArrayUtils.isEmpty(data.providers)) {
                installContentProviders(app, data.providers);
            }
        }
-> installContentProviders() @ActivityThread
-> installProvider() @ActivityThread
        final java.lang.ClassLoader cl = c.getClassLoader();
        localProvider = (ContentProvider)cl.loadClass(info.name).newInstance();
        localProvider.attachInfo(c, info);
    c 是context,info是ProviderInfo对象。info.name是provider的名字。
    由于FileProvider中重写了attachInfo(),所以,这里的localProvider.attachInfo()将执行FileProvider的attachInfo()。
-> attachInfo() @FileProvider
        super.attachInfo(context, info); // 调用父类ContentProvider的attachInfo(),设置 ContentProvider 的各种属性,并调用Provider 的onCreate()
        mStrategy = getPathStrategy(context, info.authority);
        getPathStrategy()解析filepath_data.xml文件(在本文中的例子)。

-> getPathStrategy() @FileProvider
-> parsePathStrategy() @FileProvider
    parsePathStrategy()用来解析filepath_data.xml文件,这对理解filepath_data.xml文件很有帮助。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容