源码例子:
https://github.com/StarsAaron/CameraDemo/tree/master
在Android7.0版本上,Android系统强制执行了StrictMode API 政策,禁止向你的应用外公开File://URI。如果一项包含文件File://URI类型的Intent离开你的应用,应用失败,并出现FileUriExposedException,比如系统相机拍照,裁剪照片。
FileProvider继承于ContentProvider,所以我们需要在manifest中注册Provider
1) 在manifest里注册provider
<application
...>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
provider>
application>
exported必须要求为false,为true则会报安全异常。grantUriPermissions为true,表示授予URI临时访问权限。
2) 指定共享目录
res\ xml\ file_paths.xml
<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>
中可以定义以下子节点
子节点 | 对应路径 |
---|---|
files-path | Context.getFilesDir() |
cache-path | Context.getCacheDir() |
external-path | Environment.getExternalStorageDirectory() |
external-files-path | Context.getExternalFilesDir(null) |
external-cache-path | Context.getExternalCacheDir() |
file://到content://的转换规则:
file:///data/data/com.xxx/files/images/2016/pic.png
->
content://android:authorities
+
"images/" name="my_images"/>
->
content://com.xxx.fileprovider/my_images/2016/pic.png
需要注意的是,文件的路径必须包含在xml中,也就是2.1中必须能找到一个匹配的子节点,否则会抛出异常:java.lang.IllegalArgumentException
在打开相册的需求时,有的手机厂商会在系统根目录放入一些图像来供用户体验,所有当你选择了这些就会出现onFileUriExposed的异常,所以建议加上root-path标签
3) 使用FileProvider
FileProvider.getUriForFile(this, getPackageName() + “.provider”, oriPhotoFile);
有两种设置文件访问权限
注意:如果manifest.xml没有定义android:grantUriPermissions=”true”或者定义为”false”,都需要在代码中动态定义
List 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);
}
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
第一个是根据File获取FileProvider管理的Uri,第二个是对Intent授予读写私有目录的权限。
打开系统相册的Action有三种:
setType(“image/*”)情况下:
从图库选择图像返回的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…
content://media/external/images/media/35144
这种格式,转换成路径后可以直接用
content://com.android.providers.media.documents/document/image%3A35144
这种格式,转换成路径后不能直接用。
坑1:4.4+ 系统打开相册的不同Action
坑2 :6.0+ 动态权限适配
坑3:7.0+ FileProvider对设备内容Uri的处理
坑4:小米手机的FileProvider共享目录配置(也可以说根目录下的资源文件目录配置)
添加权限,6.0需要动态权限申请
<uses-feature android:name="android.hardware.camera"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
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);
// 动态grant权限
// 如果在xml中已经定义android:grantUriPermissions="true"
// 则只需要intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);即可
// List 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);
// }
Log.i(TAG, "openCamera_imgPathOri:" + imgPathOri);
Log.i(TAG, "openCamera_imgUriOri:" + imgUriOri.toString());
}
}
private void openGallery() {
Intent intent = new Intent();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {//4.4及以上
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
} else {//4.4 以下
intent.setAction(Intent.ACTION_GET_CONTENT);
// intent.setAction(Intent.ACTION_PICK);
}
intent.setType("image/*");
startActivityForResult(intent, REQUEST_OPEN_GALLERY);
}
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) {
// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
// imgUriCrop = Uri.fromFile(cropPhotoFile);
// }else{
// imgUriCrop = FileProvider.getUriForFile(this, getPackageName() + ".provider", cropPhotoFile);
// }
//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);
}
}
附加选项 | 数据类型 | 描述 |
---|---|---|
crop | String 发送裁剪信号 | |
aspectX | int | X方向上的比例 |
aspectY | int | Y方向上的比例 |
outputX | int | 裁剪区的宽 |
outputY | int | 裁剪区的高 |
scale | boolean | 是否保留比例 |
return-data | boolean | 是否将数据保留在Bitmap中返回 |
data | Parcelable | 相应的Bitmap数据 |
circleCrop | String | 圆形裁剪区域? |
MediaStore.EXTRA_OUTPUT (“output”) | URI | 将URI指向相应的file:///… |
outputFormat | String | 输出格式,一般设为Bitmap格式:Bitmap.CompressFormat.JPEG.toString() |
noFaceDetection | boolean | 是否取消人脸识别功能 |
return-data
:是将结果保存在data中返回,在onActivityResult中,直接调用intent.getdata()就可以获取值了,这里设为fase,就是不让它保存在data中
MediaStore.EXTRA_OUTPUT
:由于我们不让它保存在Intent的data域中,但我们总要有地方来保存我们的图片啊,这个参数就是转移保存地址的,对应Value中保存的URI就是指定的保存地址。至于这两个参数能不能同时设为输出,这个我也不清楚……
实际数据:
camera content://com.danielfu.camerademo.provider/images/storage/emulated/0/Android/data/com.danielfu.camerademo/files/Pictures/OriPicture/HomePic_20180126_163642902806005.jpg
gallery
content://com.android.providers.media.documents/document/image%3A126047
/storage/emulated/0/tencent/MicroMsg/WeiXin/wx_camera_1516844243127.jpg
crop
file:///storage/emulated/0/Android/data/com.danielfu.camerademo/files/Pictures/CropPicture/HomePic_20180126_1637511666324121.jpg