Android 7.0 FileUriExposedException 的处理
前言
- 最近在做手机权限适配,在做到使用照相机权限的时候遇到了一个问题 FileUriExposedException ,一开始百思不得其解在AndroidN 之前没有问题,但是在N之后就会爆出这个问题,在查看了官方AndroidN新特性的说明后发现了问题。当然官方给了解决方案:官方给出的解决方式是通过 FileProvider 来为所共享的文件 Uri 添加临时权限。
照相机权限处理
- 调用照相机的时候需要将一个URI传到照相机的应用里,用来作为图片的存储路径,这就违背了Android N的要求,如上图所说禁止应用私有文件对外公开(Android手机越来越看中安全挺好的,适配挺麻烦),所以需要做些处理
- 之前的照相机的调用
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
uri = Uri.fromFile(mFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, PHOTOHRAPH);
- 7.0适配,URI的获取方式不再是Uri.fromFile(mFile),而是需要通过FileProvider获取 uri = FileProvider.getUriForFile(this, “包名.fileprovider”, mFile),而且同时需要为他赋予临时权限 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);不然会报错,程序会崩溃
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//7.0以上 的拍照文件必须在storage/emulated/0/Android/data/包名/files/pictures文件夹
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
uri = FileProvider.getUriForFile(this, "cn.com.jsj.GCTravelTools.fileprovider", mFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, PHOTOHRAPH);
系统图片裁剪
- 因为是做头像处理,拍照获取图片后需要裁剪,而本人不太喜欢用第三方,所以就用系统的裁剪功能,然后也遇到了坑,其实都是一个问题
try {
//剪切后的图片存储位置
// 调用系统中自带的图片剪裁
Intent intent = new Intent("com.android.camera.action.CROP");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
resultUri = Uri.fromFile(mCutResultFile);
// 调用系统中自带的图片剪裁
intent.setDataAndType(uri, IMAGE_UNSPECIFIED);
intent.putExtra("crop", "true");
intent.putExtra("scale", true);
//裁剪框的比例
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
//输出图片大小
intent.putExtra("outputX", 300);
intent.putExtra("outputY", 300);
intent.putExtra("outputFormat", Bitmap.CompressFormat.PNG.toString());
intent.putExtra("noFaceDetection", true);
intent.putExtra("return-data", false);
//设置裁剪照片完成后图片的存放地址
intent.putExtra(MediaStore.EXTRA_OUTPUT, resultUri);
startActivityForResult(intent, PHOTORESOULT);
} catch (Exception e) {
e.printStackTrace();
}
- 一些注意的问题:裁剪后的图片通过Intent的putExtra(“return-data”,true)方法进行传递,miui系统问题就出在这里,return-data的方式只适用于小图,miui系统默认的裁剪图片可能裁剪得过大,或对return-data分配的资源不足,造成return-data失败。
解决思路是:裁剪后,将裁剪的图片保存在Uri中,在onActivityResult()方法中,再提取对应的Uri图片转换为Bitmap使用。
其实大家直观也能感觉出来,Intent主要用于不同Activity之间通信,并不适用于传递图片之类的大数据。于是当A生成一个大数据要传递给B,往往不是通过Intent直接传递,而是在A生成数据的时候将数据保存到C,B再去调用C,C相当于一个转换的中间件。所以在这里也会遇到上述相同的问题,因此调用的时候需要赋予临时权限,可参考此博客
具体使用流程
- 在 标签下添加 FileProvider 节点
<provider android:name="android.support.v4.content.FileProvider"
android:authorities="包名.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
android:authority 属性指定要用于 FileProvider 生成的 content URI 的 URI 权限,这里推荐使用 包名.fileprovider 以确保其唯一性。
的 子元素指向一个 XML 文件,用于指定要共享的目录。
- 在 res/xml 目录下创建文件 file_paths.xml 内容如下:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="picture" //随便起,不影响
path="." />
</paths>
关于这里面的属性可以做个简单说明
做好前期准备工作后,上完整的代码
private final String IMAGE_UNSPECIFIED = "image/*";
private final int PHOTOHRAPH = 201;// 拍照
private final int PHOTOZOOM = 202; // 缩放
private final int PHOTORESOULT = 203;// 结果
private File mFile; //拍照图片存储路径
private File mCutResultFile; //裁剪图片存储路径
private Uri uri = null;
String photoPic = System.currentTimeMillis() + "temp1.png";
String cutPic = System.currentTimeMillis() + "cutResult.png";
private void initIcon() {
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
mFile = new File(storageDir + File.separator + photoPic);
try {
if (mFile.exists()) {
mFile.delete();
}
mFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
mCutResultFile = new File(storageDir + File.separator + cutPic);
try {
if (mCutResultFile.exists()) {
mCutResultFile.delete();
}
mCutResultFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 图片上传
*
* @param i
*/
private void uploadPictures(int i) {
try {
*
*
*
if (i == R.id.btn_common_open_camera) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//7.0以上 的拍照文件必须在storage/emulated/0/Android/data/包名/files/pictures文件夹
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
uri = FileProvider.getUriForFile(this, "cn.com.jsj.GCTravelTools.fileprovider", mFile);
} else {
uri = Uri.fromFile(mFile);
}
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, PHOTOHRAPH);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// 拍照
if (requestCode == PHOTOHRAPH) {
startPhotoZoom(uri);
}
if (data == null)
return;
if (requestCode == PHOTORESOULT) {
//节省内存的办法
Bitmap mBitmap = SaImageUtils.getScaleBitmap(this, resultUri.getPath());
portraitBitmap = mBitmap;
if (null != mBitmap) {
mFltTicketPic = BitmapToBytes(mBitmap);
savePortraitInfo();
}
}
}
Uri resultUri;
/**
* 图片剪裁功能
*/
private void startPhotoZoom(Uri uri) {
try {
//剪切后的图片存储位置
// 调用系统中自带的图片剪裁
Intent intent = new Intent("com.android.camera.action.CROP");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
resultUri = Uri.fromFile(mCutResultFile);
// 调用系统中自带的图片剪裁
intent.setDataAndType(uri, IMAGE_UNSPECIFIED);
intent.putExtra("crop", "true");
intent.putExtra("scale", true);
//裁剪框的比例
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
//输出图片大小
intent.putExtra("outputX", 300);
intent.putExtra("outputY", 300);
intent.putExtra("outputFormat", Bitmap.CompressFormat.PNG.toString());
intent.putExtra("noFaceDetection", true);
intent.putExtra("return-data", false);
//设置裁剪照片完成后图片的存放地址
intent.putExtra(MediaStore.EXTRA_OUTPUT, resultUri);
startActivityForResult(intent, PHOTORESOULT);
} catch (Exception e) {
e.printStackTrace();
}
}
另附
/**
* 描述:压缩文件图片位图,图片长宽不处理
*
* @param ctx
* @param filePath
* @return
*/
public static Bitmap getScaleBitmap(Context ctx, String filePath) {
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inJustDecodeBounds = true;
Bitmap bmp = BitmapFactory.decodeFile(filePath, opt);
int bmpWidth = opt.outWidth;
int bmpHeght = opt.outHeight;
WindowManager windowManager = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
Display display = windowManager.getDefaultDisplay();
int screenWidth = display.getWidth();
int screenHeight = display.getHeight();
opt.inSampleSize = 1;
if (bmpWidth > bmpHeght) {
if (bmpWidth > screenWidth)
opt.inSampleSize = bmpWidth / screenWidth;
} else {
if (bmpHeght > screenHeight)
opt.inSampleSize = bmpHeght / screenHeight;
}
opt.inJustDecodeBounds = false;
bmp = BitmapFactory.decodeFile(filePath, opt);
return bmp;
}
注意
除了上面这个问题,在 API Level 24(Android 7.0)之前开发的分享图文、浏览编辑本地图片、共享互传文件等功能如果没有使用 FileProvider 来生成 URI 的话,在 Android 7.0 上就必须做这种适配了,所以平时建议大家多关注 Android 新的 API ,尽早替换已被官方废弃的 API ,实际上 FileProvider 在 API Level 22(Android 5.1) 已经添加了。