美文网首页
Android FileProvider

Android FileProvider

作者: 董成鹏 | 来源:发表于2020-07-09 20:31 被阅读0次

    看这里
    Android 7之后, 在应用间 传递 file://形式的Uri会直接报错: FileUriExposedException.

    所以, 网上的很多调用相机拍照获取图片的案例都不能用, 比如这个

        public void takePhotoNoCompress(View view) {
            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();
    
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
                startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
            }
        }
    

    因为使用了 file 格式的Uri, 所以运行会出错.

    Android的这个策略是: 禁止我们的应用, 向外部公开 file 格式的 Uril. 所以在应用内部使用 file格式的Uri是可以的, 但是一旦 intent离开我们的应用, 就会出异常.

    Android7要求在应用之间共享文件, 必须使用 content格式的Uril, 最简单的是使用 FileProvider 类.

    使用方法如下:

    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.zhy.android7.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
    

    需要注意的是, 最新的 v4 包中已经不包含 FileProvider了, 全部移动到 androidx 中了, 所以应该使用android:name="androidx.core.content.FileProvider",

    然后编写 file_paths.xml文件

    <?xml version="1.0" encoding="utf-8"?>
    <paths xmlns:android="http://schemas.android.com/apk/res/android">
        <root-path name="root" path="" />
        <files-path name="files" path="" />
        <cache-path name="cache" path="" />
        <external-path name="external" path="" />
        <external-files-path name="name" path="path" />
         <external-cache-path name="name" path="path" />
    </paths>
    

    FileProvider会根据这个文件中的内容, 为对应的路径生成content格式的uri.

    path节点支持以下子节点
    <?xml version="1.0" encoding="utf-8"?>
    <paths xmlns:android="http://schemas.android.com/apk/res/android">
    <root-path name="root" path="" />
    <files-path name="files" path="" />
    <cache-path name="cache" path="" />
    <external-path name="external" path="" />
    <external-files-path name="name" path="path" />
    <external-cache-path name="name" path="path" />
    </paths>

    <root-path/> 代表设备的根目录new File("/");
    <files-path/> 代表context.getFilesDir()
    <cache-path/> 代表context.getCacheDir()
    <external-path/> 代表Environment.getExternalStorageDirectory()
    <external-files-path>代表context.getExternalFilesDirs()
    <external-cache-path>代表getExternalCacheDirs()

    每个子节点有两个属性: name 和 path,
    name用在生成的uri中, 可以展示给用户.
    path是我们的真实的文件路径, 这个不会放在uri中, path必须是一个目录, 不能是单个的文件. 不能通过path来共享单个文件, 也不能指定通配符来共享该目录的一部分文件.

    接下来就可以调用相机拍照了

           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 = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
                startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
            }
    

    这种方法在7.0的手机上没有问题, 但是在4.4上就不行了. 在4.4上别的应用因为权限问题访问不了我们的目录, 这个时候我们需要通过Context的 grantUriPermission来临时授予权限

    List<ResolveInfo> resInfoList = context.getPackageManager()
                .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
    for (ResolveInfo resolveInfo : resInfoList) {
        String packageName = resolveInfo.activityInfo.packageName;
        context.grantUriPermission(packageName, uri, flag);
    }
    

    根据Intent查出来的应用都给授权

    下面是完整代码

    public void takePhotoNoCompress(View view) {
        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 = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
    
            List<ResolveInfo> resInfoList = getPackageManager()
                    .queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
            for (ResolveInfo resolveInfo : resInfoList) {
                String packageName = resolveInfo.activityInfo.packageName;
                grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            }
    
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
            startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
        }
    }
    

    其实新老版本的主要差别就是在于Uri的获取方式不对, 所以, 也可以偷懒

    Uri fileUri = null;
    if (Build.VERSION.SDK_INT >= 24) {
        fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
    } else {
        fileUri = Uri.fromFile(file);
    }
    

    但是这样的话, 还是需要临时授权.

    每次都要给遍历到的Activity临时授权, 肯定比较麻烦, Android还提供了一个比较方便的方式, 那就是通过Intent的Flag.
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    只要加上这两个flag, 目标App就有临时权限了.

    目前来看, 这个 addFlag 生效, 只在 Camera 相关的intent中有效. 这是因为在FrameWork中, 针对几个 Camera 相关的 intent做了特殊处理, 把 OUT_PUT Uri封装到ClipData中, ClipData可以忽略其中Intent或者Uri的本来权限, 只使用外部Intent的权限

    if (MediaStore.ACTION_IMAGE_CAPTURE.equals(action)
            || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(action)
            || MediaStore.ACTION_VIDEO_CAPTURE.equals(action)) {
        final Uri output;
        try {
            output = getParcelableExtra(MediaStore.EXTRA_OUTPUT);
        } catch (ClassCastException e) {
            return false;
        }
        if (output != null) {
            setClipData(ClipData.newRawUri("", output));
            addFlags(FLAG_GRANT_WRITE_URI_PERMISSION|FLAG_GRANT_READ_URI_PERMISSION);
            return true;
        }
    }
    

    尤其要注意的是, addFlag一般用于 setData, setDataAndType和setClipData, 因为 setClipData是5.0才添加的, 所以在5.0以下这种方式没有效果.

    相关文章

      网友评论

          本文标题:Android FileProvider

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