Android 7.0拍照/相册/截取图片FileProvider使用
2017-05-09 19:01
731 查看
Android 7.0拍照/相册/截取图片FileProvider使用
Android 70拍照相册截取图片FileProvider使用GitHub地址
HIT THE PIT
需求
实现解析
一 创建需要保存原图和裁剪图的File
从原图File获取原图的Uri
1 注册Privider
2 使用FileProvider
二 调用相机拍照并保存在SD中
三 打开系统相册
四 裁剪图像
总结
后话
参考资料
GitHub地址
https://github.com/DanialFu/CameraDemoHIT THE PIT
坑1:4.4+ 系统打开相册的不同Action坑2 :6.0+ 动态权限适配
坑3:7.0+ FileProvider对设备内容Uri的处理
坑4:小米手机的FileProvider共享目录配置(也可以说根目录下的资源文件目录配置)
需求
Created with Raphaël 2.1.01.创建需要保存原图和裁剪图的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; }
注意: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); }
为了提高应用私有目录的安全性,在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>
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>
通过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); }
二. 调用相机拍照并保存在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;
}
}
三. 打开系统相册
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; }
打开系统相册的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); } }
裁剪图像同样是调用系统程序,所以,对于7.0以上和以下的版本一样要分别处理。
<7.0 Uri的类型不受限制,可传入Scheme为file和content类型的Uri
>7.0 Uri的类型就要通过FileProvider和grantUriPermission处理后才可以传入,所以只可传入Scheme为content类型的Uri
总结
由于Google官方对于安全的强制要求,我们必须对于7.0以上的版本进行私有目录文件安全控制。而安全策略的主角就是FileProviderFileProvider 定义了一套加密策略,而策略规则就是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区别
相关文章推荐
- Android 7.0拍照/相册/截取图片FileProvider使用
- Android实现拍照截取和相册图片截取
- android 7.0从相册或拍照获取图片并剪切,包含权限获取
- Android 7.0 FileProvider的使用
- Android - Android 7.0 拍照,相册选择和系统图片裁剪和删除适配
- Android手机拍照,相册拿取图片,压缩上传,7.0权限
- MVP模式的Android 调用系统拍照,相册,剪裁,适配到7.0,修复拍照图片旋转问题
- android调用系统相册选图或相机拍照后对图片进行剪切无内存溢出支持7.0
- Android 相册选择照片或拍照获取图片系统6.0及7.0实现
- Android拍照及图片裁剪、调用系统相册(兼容6.0以上权限处理及7.0以上文件管理)
- Android中使用file.createNewFile()无法创建文件的问题(例如保存拍照的图片到本地)
- Android使用WebView从相册/拍照中添加图片
- android webview使用html5<input id="input" type="file"/> 上传相册、拍照照片
- android选择系统相机拍照和系统相册,裁剪图片并保存和设置头像,适配至7.0
- android 7.0 使用FileProvider在应用间共享文件(相机适配)
- Android 7.0 FileProvider的使用
- Android 使用腾讯X5 Webview浏览器拍照或从相册上传图片
- Android使用WebView从相册/拍照中添加图片
- Android 调用系统相机拍照(适配7.0)、从图库选择图片、从数据库读取联系人信息、相册图片、扫描文件
- 如何兼容所有Android版本选择照片或拍照然后裁剪图片--基于FileProvider和动态权限的实现