欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《Android 基础(四十七)》FileProvider

程序员文章站 2022-05-14 09:06:51
...

简介

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

《Android 基础(四十七)》FileProvider

为File生成Content Uri

  • 创建需要用Uri表示的文件File
  • 使用getUriForFile()方法获取对应的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授权的步骤:

  • 调用Context.grantUriPermission(package, Uri, mode_flags)。package为包名,Uri为需要临时授权的content Uri。mode_flags可以为FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION,根据需求而定。通过revokeUriPermission() 或者重启取消授权。
  • Intent中setData方法设置Uri
  • Intent setFlags方法设置FLAG_GRANT_READ_URI_PERMISSION 或者FLAG_GRANT_WRITE_URI_PERMISSION。
  • 发送Intent到另一个App

传递Uri到另一个应用

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

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

源码看看

类结构

《Android 基础(四十七)》FileProvider

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);
相关标签: Android基础