Android FileProvider

2020-07-09  本文已影响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以下这种方式没有效果.

上一篇下一篇

猜你喜欢

热点阅读