Android7.0调用相机时出现的一个错误:
android.os.FileUriExposedException: file:///storage/emulated/0/test.jpg exposed beyond app through ClipData.Item.getUri()
解决办法:在Application的onCreat()方法中添加以下代码:
// android 7.0系统解决拍照的问题
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
builder.detectFileUriExposure();
以前调用相机的时候,代码大概是这样的
Intent openCameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Uri imageUri = Uri.fromFile(mediaFile);
openCameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(openCameraIntent, TAKE_PICTURE);
但是7.0之后直接被废弃,不改就会崩溃,所以这代码肯定要改进。
首先是权限的申请,这里就不说明了,主要是这三个权限:
Manifest.permission.WRITE_EXTERNAL_STORAGE
Manifest.permission.READ_EXTERNAL_STORAGE
Manifest.permission.CAMERA
7.0之后的不同之处在于用content://uri来代替file://uri,所以需要用ContentProvider去访问文件,FileProvider是很好的选择。
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="你的包名"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
有必要说明一下属性:
authorities:值一般是"项目的包名 + .provider",也可以不是,使用FileProvider.getUriForFile方法时参数和清单文件注册时的保持一致才能正常使用。
exported:是否对外开放
grantUriPermissions:是否授予临时权限,设置为true。
标签里面是用来指定共享的路径。
就是我们的共享路径配置的xml文件,可以自己命名。该文件放在res/xml文件夹下,若没有xml文件夹,自己创建一个。文件取名为filefile_pathspaths.xml。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path
name="my_images"
path="images" />
<!--<external-path> 可被替换成<external-files-path>、<external-cache-path>、<file-path>、<cache-path>等。下面给出五个的区别:-->
<!--<external-path>:共享外部存储卡,对应/storage/emulated/0目录,即Environment..getExternalStorageDirectory()-->
<!--<external-files-path>:共享外部存储的文件目录,对应/storage/emulated/0/Android/data/包名/files,即Context.getExternalFilesDir()-->
<!--<external-cache-path>:共享外部存储的缓存目录,对应/storage/emulated/0/Android/data/包名/cache,即Context.getExternalCacheDir()-->
<!--<file-path>:共享内部文件存储目录,对应 /data/data/包名/files目录,即Context.getFilesDir()-->
<!--<cache-path>:共享内部缓存目录,对应 /data/data/包名/cache目录,即Context.getCacheDir()-->
<!--name:随便定义-->
<!--path :需要临时授权访问的路径。可以为空,表示指定目录下的所有文件、文件夹都可以被共享-->
</paths>
然后就是URI的获取
/**
* 获取URI,android7.0以后的uri发生改变
*
* @param context
* @return
*/
public static Uri getImageUri(Context context, String imgName) {
String path = context.getFilesDir() + File.separator + "images" + File.separator;
File file = new File(path, imgName);
if (!file.getParentFile().exists())
file.getParentFile().mkdirs();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return FileProvider.getUriForFile(context, context.getPackageName(), file);
} else {
return Uri.fromFile(file);
}
}
需要注意的是,这边参数需要和清单文件里面声明的保持一致。
/**
* 拍照
*
* @param activity
* @param cameraUri
* @param requestCode
*/
public static void takePhoto(Activity activity, Uri cameraUri, int requestCode) {
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT, cameraUri);
activity.startActivityForResult(intent, requestCode);
}
cameraUri就是调用getImageUri获取到的uri,因为这个uri在onActivityResult中有其他用途,所以需要在相关的activity声明变量持有。
通过上诉代码就能正常调用拍照了,然后就是onActivityResult的一些业务逻辑了。
protected void setActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == RESULT_CANCELED) {
return;
}
outputUri = getOutputHeaderUri(IMAGE_HEADER_NAME);
switch (requestCode) {
case CODE_GALLERY_REQUEST:
if (data != null) {
cropRawPhoto(mActivity, data.getData(), outputUri, CODE_RESULT_REQUEST);
}
break;
case CODE_CAMERA_REQUEST:
if (resultCode == RESULT_OK) {
cropRawPhoto(mActivity, cameraUri, outputUri, CODE_RESULT_REQUEST);
}
case CODE_RESULT_REQUEST:
if (resultCode == RESULT_OK) {
try {
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(outputUri));
mIvHeader.setImageBitmap(bitmap);
//上传给服务器
} catch (Exception e) {
e.printStackTrace();
}
}
break;
default:
break;
}
}
之所以没有在 (requestCode == RESULT_CANCELED) 判断中加入data的判空,是因为有些时候返回值就是null的,比如上面的那个拍照方法调用的返回结果就是个null。
cropRawPhoto裁剪功能,拍照啊,选取图片得到的图片可能并不符合需求,需要经过裁剪,比如头像的设置。
/**
* 裁剪图片
*
* @param activity
* @param inputUri
* @param outputUri
* @param requestCode
*/
public static void cropRawPhoto(Activity activity, Uri inputUri, Uri outputUri, int requestCode) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(inputUri, "image/*");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 300);
intent.putExtra("outputY", 300);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
activity.startActivityForResult(intent, requestCode);
}
需要强调的是 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
是因为app提示无法保存裁剪后的图片,查询之后增加的。
/**
* 获取一个保存的uri
*
* @param name
* @return
*/
public static Uri getOutputHeaderUri(String name) {
return Uri.parse("file://" + "/" + Environment.getExternalStorageDirectory().getPath() + "/" + name);
}
这个方法我本来是直接使用getImageUri获取的,结果反而报错了,查询后发现这个确实要使用原来的。
还有个方法
/**
* 选取相册中的图片
*
* @param activity
* @param requestCode
*/
public static void choseHeadImageFromGallery(Activity activity, int requestCode) {
Intent intentFromGallery = new Intent();
intentFromGallery.setType("image/*");
intentFromGallery.setAction(Intent.ACTION_PICK);
activity.startActivityForResult(intentFromGallery, requestCode);
}
基本上代码都齐了,另外就是一些变量和常量
//请求码
private static final int CODE_GALLERY_REQUEST = 0xa0;
private static final int CODE_CAMERA_REQUEST = 0xa1;
private static final int CODE_RESULT_REQUEST = 0xa2;
//拍照保存的URI和确定上传到后台的图片URI
private Uri cameraUri;
private Uri outputUri;
private static final String IMAGE_FILE_NAME = "temp_header.jpg";
private static final String IMAGE_HEADER_NAME = "header.jpg";
至于点击事件和权限的获取个人实现方式不同就不越俎代庖了,记得是在获取成功之后才调用这些方法的。
有什么错误的地方请大佬多多指教!