在Android7.0之前,如果你想调用系统相机拍照可以通过以下代码来进行:
File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
if (!file.getParentFile().exists())file.getParentFile().mkdirs();
Uri imageUri = Uri.fromFile(file);
Intent intent = new Intent();
//设置Action为拍照
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
//将拍取的照片保存到指定URI
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent,1);
但是在android 7.0之后的手机上用以上代码打开相机、相册会报如下错误:
android.os.FileUriExposedException: ** exposed beyond app through Intent.getData()
原因:
在Android7.0系统上,Android 框架强制执行了 StrictMode API 政策禁止向你的应用外公开 file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常,如调用系统相机拍照,或裁切照片。
解决方法:
官方的解决方法(https://developer.android.google.cn/training/secure-file-sharing/setup-sharing.html)
1、在manifest清单文件中注册provider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="包名.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
也就是在application内加了一个provider:
- name是固定的;
- android:authorities推荐写您的应用包名+“.fileprovider”,其实这里不一定要写fileprovider,您也可以随便写,只要与后面使用FileProvider.getUriForFile()这个方法中的第二个参数authority对应起来即可;
- android:grantUriPermissions固定true,表示uri访问授权;
- android:exported固定的false,我试着写了true报安全异常。
- android:resource表示我们app要共享文件的路径的资源文件。
2.res文件夹下,新建一个xml文件夹,名字就是上一步 android:resource=”@xml/file_paths”对应的内容
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path path="" name="camera_photos" />
</paths>
</resources>
为了指定访问的目录我们需要在资源(res)目录下创建一个xml目录
- 命名为“file_paths”(名字可以随便起,只要和第一步中在manifest注册的provider所引用的resource保持一致即可)的资源文件。
- 上述代码中 path=”“,是有特殊意义的,它指的是根目录,也就是说您可以向其它的应用访问根目录及其子目录下任何一个文件了,如果您将path设为 path=”pictures”,那么它代表着根目录下的pictures目录(eg:/storage/emulated/0/pictures),这时您访问pictures目录范围之外的文件是不行的。
- files-path代表的根目录: Context.getFilesDir()
- external-path代表的根目录: Environment.getExternalStorageDirectory()
- cache-path代表的根目录: getCacheDir()
3.使用FileProvider
上述准备工作做完之后,现在我们就可以使用FileProvider了。
还是以调用系统相机拍照为例,我们需要将上述拍照代码修改为如下:
/*
*相册,选择一张图片
*/
private void getPhoto(Context context) {
// 这里用时间命名是发现用固定名命名后第二次裁剪图片任然是第一次的图,没有覆盖上一次图片资源;7.0之前固定名会替换
File file = new File(Environment.getExternalStorageDirectory(), "/temp/" + System.currentTimeMillis() + ".jpg");
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
// 通过FileProvider创建一个content类型的Uri
Uri imageUri = FileProvider.getUriForFile(context, "包名.fileprovider", file);
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 设置Action为拍照
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
// 将拍取的照片保存到指定URI
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
// 开启一个带有返回值的Activity,请求码为PHOTO_REQUEST_GALLERY
startActivityForResult(intent, MainActivity.PHOTO_REQUEST_GALLERY);
}
上述代码通过FileProvider的Uri getUriForFile (Context context, String authority, File file)
静态方法来获取Uri,该方法中authority参数就是清单文件中注册provider的android:authorities=”包名.fileprovider”。
裁剪图片同理,修改后:
/*
* 剪切图片
*/
private void crop(String path, int outWith, int outHeight) {
File file = new File(path);
if (!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
Uri uri;
// 裁剪图片意图
Intent intent = new Intent("com.android.camera.action.CROP");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
//添加这一句表示对目标应用临时授权该Uri所代表的文件
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//通过FileProvider创建一个content类型的Uri
uri = FileProvider.getUriForFile(this, "包名.fileProvider", file);
} else{
uri = Uri.fromFile(file);
}
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", "true");
// 裁剪框的比例,1:1
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
// 裁剪后输出图片的尺寸大小
intent.putExtra("outputX", outWith);
intent.putExtra("outputY", outHeight);
// 图片格式
intent.putExtra("outputFormat", "JPEG");
// 取消人脸识别
intent.putExtra("noFaceDetection", true);
/**
* 此方法返回的图片只能是小图片(测试为高宽160px的图片)
* 故将图片保存在Uri中,调用时将Uri转换为Bitmap,此方法还可解决miui系统不能return data的问题
*/
//intent.putExtra("return-data", true); // 重点:小米的系统这个会使返回数据为null,改为下面方法传递数据
//uritempFile为Uri类变量,实例化uritempFile
uritempFile = Uri.parse("file://" + "/" + Environment.getExternalStorageDirectory().getPath() + "/" + "small.jpg");
intent.putExtra(MediaStore.EXTRA_OUTPUT, uritempFile);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
// 开启一个带有返回值的Activity,请求码为PHOTO_REQUEST_CUT
startActivityForResult(intent, MainActivity.PHOTO_REQUEST_CUT);
}
然后在你的onActivityResult回调中这样接收
if (requestCode == V3MainActivity.PHOTO_REQUEST_CUT) {
//将Uri图片转换为Bitmap
try {
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uritempFile));
//TODO 将获取的bitmap展示在你的控件上
} catch (FileNotFoundException e) {
ToastUtils.showShort("未找到裁剪图片");
}
}
ps:另外发现严苛模式(StrictMode)也可以避免这个FileUriExposedException问题,在您的Application中OnCreate()方法中加入以下代码即可。这样您可以按照7.0之前方法调用相机。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
}
但毕竟这方法不是官方推荐的,所以知道就行,不建议使用。
更新:
我们会发现拍照出来的照片在手机相册中没有显示出来,而自己去翻文件夹却又能够找到该照片,这时想要在手机相册中显示刚拍摄的图片需要在回调函数里加入如下代码,发送个广播通知系统。
//在手机相册中显示刚拍摄的图片
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri contentUri = Uri.fromFile(file);
mediaScanIntent.setData(contentUri);
sendBroadcast(mediaScanIntent);
参考:
官方推荐解决方案
Android 7.0手机打开相机或相册报错解决方案
Android 7.0 文件读取适配,及适配相机及裁剪图片
严苛模式
为了向别人、向世界证明自己而努力拼搏,而一旦你真的取得了成绩,才会明白:人无须向别人证明什么,只要你能超越自己。