Android 7.0拍照/相册/截取图片FileProvider使用

Android 7.0拍照/相册/截取图片FileProvider使用

GitHub地址

https://github.com/DanialFu/CameraDemo

HIT THE PIT

  • 坑1:4.4+ 系统打开相册的不同Action
  • 坑2 :6.0+ 动态权限适配
  • 坑3:7.0+ FileProvider对设备内容Uri的处理
  • 坑4:小米手机的FileProvider共享目录配置(也可以说根目录下的资源文件目录配置)

需求

1.创建需要保存原图和裁剪图的File调用系统相机?a1.从原图File获取原图Uria2.调用相机拍照并保存在SD中2.裁剪图像获取到裁剪的图像处理并显示b1.打开系统相册b2.获取相册选择的图像Urib3.将Uri格式化yesno

实现解析

一. 创建需要保存原图和裁剪图的File

/**
     * 创建原图像保存的文件
     * @return
     * @throws IOException
     */
    private File createOriImageFile() throws IOException {
        String imgNameOri = "HomePic_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        File pictureDirOri = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/OriPicture");
        if (!pictureDirOri.exists()) {
            pictureDirOri.mkdirs();
        }
        File image = File.createTempFile(
                imgNameOri,         /* prefix */
                ".jpg",             /* suffix */
                pictureDirOri       /* directory */
        );
        imgPathOri = image.getAbsolutePath();
        return image;
    }

    /**
     * 创建裁剪图像保存的文件
     * @return
     * @throws IOException
     */
    private File createCropImageFile() throws IOException {
        String imgNameCrop = "HomePic_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        File pictureDirCrop = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/CropPicture");
        if (!pictureDirCrop.exists()) {
            pictureDirCrop.mkdirs();
        }
        File image = File.createTempFile(
                imgNameCrop,         /* prefix */
                ".jpg",             /* suffix */
                pictureDirCrop      /* directory */
        );
        imgPathCrop = image.getAbsolutePath();
        return image;
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

注意:Demo中将所有保存的图像均存放在app内部存储的路径中,当app卸载后存储的资源文件也会相应的删除。所以是存储路径的选择时根据项目需求来改变的。imgPathOri & imgPathCrop Demo中仅用于打印Log日志使用

1. 从原图File获取原图的Uri
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
    imgUriOri = Uri.fromFile(oriPhotoFile);
} else {
    imgUriOri = FileProvider.getUriForFile(this, getPackageName() + ".provider", oriPhotoFile);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

为了提高应用私有目录的安全性,在7.0之后Google强制执行了“StrictMode API 政策”,用于限制访问私有文件目录的权限。如果在7.0之后调用包含file://Uri 类型的Intent离开了应用界面,会抛出onFileUriExposed的异常。所以我们用FileProvide来”格式化”Uri,用grantUriPermissions来授权Intent所持有的权限

1.1 注册Privider

FileProvider继承于ContentProvider,所以我们需要在manifest中注册Provider

manifest.xml

    <application
                 ...>
        <!--FileProvider共享文件-->
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.danielfu.camerademo.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>
    </application>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • authorities:标识provider 一般由 [packageName/applicationId + 自定义名称] 命名
  • exported:要求必须为false,具体信息不在本文讨论
  • grantUriPermissions:是否授予Uri临时访问权限
  • meta-data:定义FileProvider所授权的资源路径

res\ xml\ file_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path path="Android/data/com.example.package.name/files/Pictures/OriPicture/" name="images" />
    <external-path path="Android/data/com.example.package.name/files/Pictures/OriPicture/" name="images" />
    <external-files-path path="files/Pictures/OriPicture" name="images"/>
    <root-path path="" name="images"/>
</paths>
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

通过paths.xml来定义FileProvider需要转换为Content Uri的文件路径。第一第二个为Demo测试使用

标签是官方文档上没有说明的,在这里需要说明下为什么会用到root-path,顾名思义他就是代表设备根目录的。在打开相册的需求时,有的手机厂商会在系统根目录放入一些图像来供用户体验,所有当你选择了这些就会出现onFileUriExposed的异常,所以建议加上root-path标签

不同的标签对应不同的存储区域,path和name标签规定了file 转换成content 的匹配规则。如果想深入了解规则参考[filecontent转换规则][file-content转换规则]

// Context.getFilesDir(). 
// getCacheDir(). 
// Environment.getExternalStorageDirectory(). 
// Context.getExternalFilesDir(). 
// Context.getExternalCacheDir().

1.2 使用FileProvider

FileProvider.getUriForFile(this, getPackageName() + “.provider”, oriPhotoFile);

intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

第一个是根据File获取FileProvider管理的Uri,第二个是对Intent授予读写私有目录的权限。

注意:如果manifest.xml没有定义Android:grantUriPermissions=”true”或者定义为”false”,都需要在代码中动态定义

List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
    String packageName = resolveInfo.activityInfo.packageName;
    grantUriPermission(packageName, imgUriOri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

二. 调用相机拍照并保存在SD中

 private void openCamera() {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);// 打开相机
        File oriPhotoFile = null;
        try {
            oriPhotoFile = createOriImageFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (oriPhotoFile != null) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
                imgUriOri = Uri.fromFile(oriPhotoFile);
            } else {
                imgUriOri = FileProvider.getUriForFile(this, getPackageName() + ".provider", oriPhotoFile);
            }
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);//私有目录读写权限
            intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUriOri);
            startActivityForResult(intent, REQUEST_OPEN_CAMERA); 
        }
    }

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode != RESULT_OK) {
        return;
    }
    switch (requestCode) {
        case REQUEST_OPEN_CAMERA:
            //imgUriOri... Just do someThing, For example crop picture
            cropPhoto(imgUriOri);
            break;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

三. 打开系统相册

    private void openGallery() {
        Intent intent = new Intent();
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
            intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
        } else {
            intent.setAction(Intent.ACTION_GET_CONTENT);
    //            intent.setAction(Intent.ACTION_PICK);
        }
        intent.setType("image/*");
        startActivityForResult(intent, REQUEST_OPEN_GALLERY);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode != RESULT_OK) {
            return;
        }
        switch (requestCode) {
            case REQUEST_OPEN_GALLERY:
                if (data != null) {
                    Uri imgUriSel = data.getData();
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        //相册会返回一个由相册安全策略定义的Uri,app使用这个Uri直接放入裁剪程序会不识别,抛出[暂不支持此类型:华为7.0]
                        //formatUri会返回根据Uri解析出的真实路径
                        String imgPathSel = UriUtils.formatUri(this, imgUriSel);
                        //根据真实路径转成File,然后通过应用程序重新安全化,再放入裁剪程序中才可以识别
                        imgUriSel = FileProvider.getUriForFile(this, getPackageName() + ".provider", new File(imgPathSel));
                        //7.0+ imgUriSel... Just do someThing, For example crop picture
                        cropPhoto(imgUriSel);
                      } else {
                       //7.0- imgUriSel... Just do someThing, For example crop picture
                       cropPhoto(imgUriSel);
                      }
                }
                break;
        }
    }

    //UriUtils.java
    @TargetApi(19)
    public static String formatUri(Context context, Uri uri) {
        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
        // DocumentProvider
        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
            // ExternalStorageProvider
            if (isExternalStorageDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                if ("primary".equalsIgnoreCase(type)) {
                    return Environment.getExternalStorageDirectory() + "/"
                            + split[1];
                }

                // TODO handle non-primary volumes
            }
            // DownloadsProvider
            else if (isDownloadsDocument(uri)) {
                final String id = DocumentsContract.getDocumentId(uri);
                final Uri contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"),
                        Long.valueOf(id));

                return getDataColumn(context, contentUri, null, null);
            }
            // MediaProvider
            else if (isMediaDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }

                final String selection = "_id=?";
                final String[] selectionArgs = new String[]{split[1]};

                return getDataColumn(context, contentUri, selection,
                        selectionArgs);
            }
        }
        // MediaStore (and general)
        else if ("content".equalsIgnoreCase(uri.getScheme())) {

            // Return the remote address
            if (isGooglePhotosUri(uri))
                return uri.getLastPathSegment();

            return getDataColumn(context, uri, null, null);
        }
        // File
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }

        return null;
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105

打开系统相册的Action有三种:ACTION_PICK,ACTION_GET_CONTENT,ACTION_OPEN_DOCUMENT。

setType(“image/*”)情况下:

  • ACTION_PICK :直接打开图库,优点是不会出现类型混乱的情况,缺点仅能打开图库而不能从其他路径中获取资源且界面较单一。

  • ACTION_GET_CONTENT:4.4以下的版本打开的是缩略图的图库,4.4以上是可选择的页面(图库/图像选择器)。

  • ACTION_OPEN_DOCUMENT:直接打开图像选择器,不可以在4.4以下使用。(官方建议4.4+使用此Action)。

图库选择图像返回的Uri:

  • <7.0 :content://media/external/images/media/…
  • >7.0 :content://com.android.providers.media.documents/document/image…

图像选择器选择图像返回的Uri:

  • <4.4 :不可打开图像选择器

  • 4.4 - 7.0 :content://media/external/images/media/…

  • >7.0 :content://com.android.providers.media.documents/document/image…

四. 裁剪图像

/**
     * 裁剪图片
     * @param uri 需要 裁剪图像的Uri
     */
    public void cropPhoto(Uri uri) {
        Intent intent = new Intent("com.android.camera.action.CROP");
        File cropPhotoFile = null;
        try {
            cropPhotoFile = createCropImageFile();
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (cropPhotoFile != null) {
            //7.0 安全机制下不允许保存裁剪后的图片
            //所以仅仅将File Uri传入MediaStore.EXTRA_OUTPUT来保存裁剪后的图像
            imgUriCrop = Uri.fromFile(cropPhotoFile);

            intent.setDataAndType(uri, "image/*"); 
            intent.putExtra("crop", true);
            intent.putExtra("aspectX", 1);
            intent.putExtra("aspectY", 1);
            intent.putExtra("outputX", 300);
            intent.putExtra("outputY", 300);
            intent.putExtra("return-data", false);
            intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUriCrop);
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            startActivityForResult(intent, REQUEST_CROP_PHOTO); 
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

裁剪图像同样是调用系统程序,所以,对于7.0以上和以下的版本一样要分别处理。

  • <7.0 Uri的类型不受限制,可传入Scheme为file和content类型的Uri
  • >7.0 Uri的类型就要通过FileProvider和grantUriPermission处理后才可以传入,所以只可传入Scheme为content类型的Uri

总结

由于Google官方对于安全的强制要求,我们必须对于7.0以上的版本进行私有目录文件安全控制。而安全策略的主角就是FileProvider

FileProvider 定义了一套加密策略,而策略规则就是demo中的file-path.xml,加密的详细过程可以参考[file-content转换规则][file-content转换规则]。

grantUriPermissions 则是向其他应用授予私有目录的读取权限,换句话说就是告诉某一应用我的Uri的加密规则。接着Intent跳转的应用就可以根据FileProvider的加密规则解析出私有目录的真实位置来进行操作。

拍照/图像选择器/裁剪都是属于系统应用,所以我们需要对7.0以上的系统应用公开app私有目录的安全策略,否则系统会抛出FileUriExposedException的异常。

在本例中,相机拍照所保存图像的Uri是有app自己定义的加密策略,所以调用系统裁剪应用可以直接传入imgUriOri。而7.0以后采用图像选择器应用返回的图像Uri的来源不可控制,导致图像的安全策略我们app并不知道,所以我们需要从未知安全策略的Uri中解析出图像真正的路径,接而转换为由app自己定义安全策略的Uri,这样系统裁剪应用才可以解析出图像。

后话

本篇因为时间和精力,对于系统存储目录并没有太多的解释,空出时间会详细分析目录以及相关知识点

这个博客前后翻了不下于60篇博文,包括官网和Stack Overflow,在这里感谢各位的开源奉献的精神。由于“抄袭”博文太多就不一一感谢了,本博文纯手打,0复制粘贴,请转载的大大们注明文章来源,尊重开源精神。

另外,最重要的一点,如果各位大大发现博文用词不当或出现bug请在第一时间联系我更改,避免错误传播。Google邮箱:unableApe@gmail.com

本例测试机型:中兴(4.0)、HTC802w(4.4.2)、红米Note(6.0)、华为荣耀8(7.0)

参考资料

Android M、N适配踩坑

file-content转换规则

系统调用相册不同的Action区别





原文地址;http://blog.csdn.net/fuhao476200/article/details/71487432

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值