FileProvider

/ Android四大组件 / 没有评论 / 277浏览

简介

FileProvider,是ContentProvider的子类,通过构建以"content://"开头的Uri取代之前以"file://"开头的Uri,以此实现应用间的文件共享。

由来

官文Android7.0行为变更说明:

对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。

要在应用间共享文件,需要改用content:// 格式的URI,并授予 URI 临时访问权限。 实现此类操作最简单的方法就是使用FileProvider。

使用方式

定义FileProvider

FileProvider本身就能根据file生成content:// Uri,所以我们并没有必要去写一个单独的FileProvider子类。但是在某些情况下,我们可以简单的继承FileProvider,修改类名来实现与FileProvider在名字上的区分,毕竟在AndroidManifest.xml中,名字相同的provider是不被允许的。

AndroidManifest.xml中申明FileProvider

<manifest>
    ...
    <application>
        ...
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
        ...
    </application>
</manifest>

明确可用文件

上面明确来resource文件为"@xml/file_paths"。那么我们就在file_paths中明确我们的可用位置。

FileProvider只能为你事先指定的目录中的文件生成内容URI。 要指定目录,请使用< paths >元素的子元素指定其存储区域和XML路径。 例如,以下路径元素告诉FileProvider你打算请求私有文件目录下的images /子目录的内容URI

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="my_images" path="images/"/>
    ...
</paths>
paths元素对应目录
< root-path/>"/"
< files-path name="name" path="path" />Context.getFilesDir()
< cache-path name="name" path="path" />Context.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" />Context.getExternalCacheDir()
< external-media-path name="name" path="path" />Context.getExternalMediaDirs()(API21+)

这样看可能不太明显,随意新建一个Android工程,打印如上内容,示例工程包名为 cn.onlyloveyd.lazyshare

这里写图片描述

为File生成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);

生成的Uri就是

content://com.mydomain.fileprovider/my_images/default_image.jpg.

临时授权Uri

给通过getUriForFile()方法返回的Uri授权的步骤:

传递Uri到另一个应用

将content:\ Uri提供给客户端应用程序的方式很多。 一种常见的方法是客户端应用程序通过调用startActivityResult()来启动应用程序,发送一个Intent以启动一个Activity,然后通过setResult() 的方式返回给客户端。

另一种方式是通过调用Intent.setClipData()方法将content:\ Uri放入ClipData对象中,然后将该对象添加到发送给客户端应用程序的Intent中即可。

源码看看

类结构

这里写图片描述

SimplePathStrategy

    static class SimplePathStrategy implements PathStrategy {
        private final String mAuthority;
        private final HashMap<String, File> mRoots = new HashMap<String, File>();

        SimplePathStrategy(String authority) {
            mAuthority = authority;
        }

        /**
         * Add a mapping from a name to a filesystem root. The provider only offers
         * access to files that live under configured roots.
         */
         //读取xml配置文件,建立名称和目录的映射表
        void addRoot(String name, File root) {
            //名字不能为空
            if (TextUtils.isEmpty(name)) {
                throw new IllegalArgumentException("Name must not be empty");
            }

            try {
                // Resolve to canonical path to keep path checking fast
                root = root.getCanonicalFile();
            } catch (IOException e) {
                throw new IllegalArgumentException(
                        "Failed to resolve canonical path for " + root, e);
            }

            mRoots.put(name, root);
        }

        @Override
        public Uri getUriForFile(File file) {
            String path;
            try {
                //获取路径
                path = file.getCanonicalPath();
            } catch (IOException e) {
                throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
            }

            // Find the most-specific root path
            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;
                }
            }

            //查询未果,说明在xml中未定义
            if (mostSpecific == null) {
                throw new IllegalArgumentException(
                        "Failed to find configured root that contains " + path);
            }

            // Start at first char of path under root
            final String rootPath = mostSpecific.getValue().getPath();
            if (rootPath.endsWith("/")) {
                path = path.substring(rootPath.length());
            } else {
                path = path.substring(rootPath.length() + 1);
            }

            // Encode the tag and path separately
            path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
            //构建content Uri,这就是最后我们拿到的内容
            return new Uri.Builder().scheme("content")
                    .authority(mAuthority).encodedPath(path).build();
        }

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

            //通过uri反向寻找,和上面的原理差不多,不赘述
            final int splitIndex = path.indexOf('/', 1);
            final String tag = Uri.decode(path.substring(1, splitIndex));
            path = Uri.decode(path.substring(splitIndex + 1));

            final File root = mRoots.get(tag);
            if (root == null) {
                throw new IllegalArgumentException("Unable to find configured root for " + uri);
            }

            File file = new File(root, path);
            try {
                file = file.getCanonicalFile();
            } catch (IOException e) {
                throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
            }

            if (!file.getPath().startsWith(root.getPath())) {
                throw new SecurityException("Resolved path jumped beyond configured root");
            }

            return file;
        }
    }

parsePathStrategy

 private static PathStrategy parsePathStrategy(Context context, String authority)
            throws IOException, XmlPullParserException {
        final SimplePathStrategy strat = new SimplePathStrategy(authority);
        
        //获取Provider信息
        final ProviderInfo info = context.getPackageManager()
                .resolveContentProvider(authority, PackageManager.GET_META_DATA);
        // 获取"android.support.FILE_PROVIDER_PATHS"对应的xml文件解析对吸纳个;
        final XmlResourceParser in = info.loadXmlMetaData(
                context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
        if (in == null) {
            throw new IllegalArgumentException(
                    "Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
        }

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

                final String name = in.getAttributeValue(null, ATTR_NAME);
                String path = in.getAttributeValue(null, ATTR_PATH);

                File target = null;
                //"root-path"
                if (TAG_ROOT_PATH.equals(tag)) {
                    target = DEVICE_ROOT;
                //"files-path"
                } else if (TAG_FILES_PATH.equals(tag)) {
                    target = context.getFilesDir();
                //"cache-path"
                } else if (TAG_CACHE_PATH.equals(tag)) {
                    target = context.getCacheDir();
                //"external-path"
                } else if (TAG_EXTERNAL.equals(tag)) {
                    target = Environment.getExternalStorageDirectory();
                //"external-files-path"
                } else if (TAG_EXTERNAL_FILES.equals(tag)) {
                    File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
                    if (externalFilesDirs.length > 0) {
                        //取数组中的第一个
                        target = externalFilesDirs[0];
                    }
                // "external-cache-path"
                } else if (TAG_EXTERNAL_CACHE.equals(tag)) {
                    File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
                    if (externalCacheDirs.length > 0) {
                        target = externalCacheDirs[0];
                    }
                // "external-media-path" L版本以上才有
                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
                        && TAG_EXTERNAL_MEDIA.equals(tag)) {
                    File[] externalMediaDirs = context.getExternalMediaDirs();
                    if (externalMediaDirs.length > 0) {
                        target = externalMediaDirs[0];
                    }
                }

                if (target != null) {
                    strat.addRoot(name, buildPath(target, path));
                }
            }
        }
        return strat;
    }

从上面这个方法可以很直观的了解到在AndroidManifest.xml文件中定义provider以及对应的共享文件路径定义xml的解析过程。以及xml中tag与真实文件路径的对应关系。

使用场景

拍照

Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                    .format(new Date()) + ".png";
            File file = new File(Environment.getExternalStorageDirectory(), filename);
            mCurrentPhotoPath = file.getAbsolutePath();

            Uri fileUri;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                fileUri = getUriForFile(context,
                context.getPackageName() +".fileprovider", file);
            } else {
                fileUri = Uri.fromFile(file);
            }

            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
            startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
        }
……
@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_TAKE_PHOTO) {
            mIvPhoto.setImageBitmap(BitmapFactory.decodeFile(mCurrentPhotoPath));
        }
        // else tip?

    }

应用安装

        // 需要自己修改安装包路径
        File file = new File(Environment.getExternalStorageDirectory(),
                "/onlyloveyd/base.apk");
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);    
        
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.setDataAndType(getUriForFile(context, file), "application/vnd.android.package-archive");
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        } else {
            intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
        }
        startActivity(intent);