在Android7.0系统上,Android 框架强制执行了 StrictMode API 政策禁止向你的应用外公开 file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常,如调用系统相机拍照,或裁切照片。
应对策略:若要在应用间共享文件,可以发送 content:// URI类型的Uri,并授予 URI 临时访问权限。 进行此授权的最简单方式是使用 FileProvider类。
下面来介绍下如何使用FileProvider解决Uri访问抛出安全异常的问题:
使用FileProvider
使用FileProvider的大致步骤如下:
(1)在AndroidManifest.xml中注册provider
注意:
android:exported:要求必须为false,为true则会报安全异常;
android:grantUriPermissions:true,表示授予 URI 临时访问权限;
android:authorities这个属性的值,建议写包名+fileprovider,当然也可以起别的字符串,但是在设备中不能出现2个及以上的APP使用到同一个authorities属性值,因为无法共存。
(2)指定共享的目录
在app/src/创建一个xml的目录,如下:
向该目录中添加一个provider_paths.xml文件(命名可以自由定义,但是需上下文一致),添加如下内容到file_paths.xml中。
代表的根目录:Context.getFilesDir()
代表的根目录:Environment.getExternalStorageDirectory()
代表的根目录:getCacheDir()
上述代码中path=“ ”,是有特殊意义的,它代码根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了,如果你将path设为path=“pictures”,那么它代表着根目录下的pictures目录(eg:/storage/emulated/0/pictures),如果你向其它应用分享pictures目录范围之外的文件是不行的。
(3)使用FileProvider加密Uri
在代码中,通过增加判断系统版本号来执行生成不同的Uri:
/**
* 解决7.0后uri安全问题
*
* @param context
* @param file
* @return
*/
public static Uri getUriForFile(Context context, File file) {
if (context == null || file == null) {
throw new NullPointerException();
}
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file);
} else {
uri = Uri.fromFile(file);
}
return uri;
}
前面介绍过,调用系统图片裁剪的方法在CropUtils,我们来看看这个类又修改了那些地方:
package com.sharetronic.util;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.support.v4.content.FileProvider;
import java.io.File;
/**
* Created by Administrator on 2018/10/26.
*/
public class CropUtils {
/**
* 解决7.0后uri安全问题
*
* @param context
* @param file
* @return
*/
public static Uri getUriForFile(Context context, File file) {
if (context == null || file == null) {
throw new NullPointerException();
}
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file);
} else {
uri = Uri.fromFile(file);
}
return uri;
}
private static String mFile;
public static String getPath(String path) {
if (mFile == null) {
mFile = path;
}
return mFile;
}
/**
*
*/
/**
* 调用系统照片的裁剪功能,修改编辑头像的选择模式(适配Android7.0)
* @param uri 系统照片的uri
* @param path 剪裁后保存的路径
* @return
*/
public static Intent invokeSystemCrop(Uri uri, String path) {
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);
}
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", "true");// crop为true是设置在开启的intent中设置显示的view可以剪裁
intent.putExtra("scale", true);
// aspectX aspectY 是宽高的比例
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 555);// outputX 是剪裁图片的宽
intent.putExtra("outputY", 578);// outputY 是剪裁图片的高
intent.putExtra("return-data", false);
intent.putExtra("noFaceDetection", false);//去除默认的人脸识别,否则和剪裁匡重叠
File out = new File(getPath(path));
if (!out.getParentFile().exists()) {
out.getParentFile().mkdirs();
}
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(out));
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
return intent;
}
}
通过上面的分析,我们已经知道如何就拍照和调用系统裁剪图片等适配7.0设备
Activity.class调用:
/**
* 显示PopupWindow,选择相册,拍照
*/
private void showPopupWindow() {
userHeadPopupwindow = new SetUserHeadPopupwindow(this, new View.OnClickListener() {
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.tv_setuserhead_from_album:
userHeadPopupwindow.dismiss();
//相册
Intent i = new Intent(
Intent.ACTION_PICK,
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(i, PHOTO_ALBUM);
break;
case R.id.tv_setuserhead_take_photo:
userHeadPopupwindow.dismiss();
//拍照
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {
if (ContextCompat.checkSelfPermission(SetUserHeadActivity.this,
android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
//先判断有没有权限 ,没有就在这里进行权限的申请
ActivityCompat.requestPermissions(SetUserHeadActivity.this,
new String[]{android.Manifest.permission.CAMERA}, CAMERA_OK);
} else {
//说明已经获取到摄像头权限了 想干嘛干嘛
takePhoto();
}
} else {
//这个说明系统版本在6.0之下,不需要动态获取权限。
}
break;
case R.id.tv_setuserhead_cancel:
userHeadPopupwindow.dismiss();
break;
}
}
// }
});
userHeadPopupwindow.showAtLocation(SetUserHeadActivity.this.
findViewById(R.id.rl_setuserhead), Gravity.BOTTOM, 0, 0);
}
拍照的代码:
//拍照的代码
private void takePhoto() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
//创建一个File
File outputImage = new File(RequestNetUrl.userHeadPath, RequestNetUrl.userHeadName);
try {
if (outputImage.exists()) {
outputImage.delete();
}
outputImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
if (outputImage != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//如果是7.0及以上的系统使用FileProvider的方式创建一个Uri
Log.e(TAG, "Build.VERSION.SDK_INT >= Build.VERSION_CODES.N");
outImage = FileProvider.getUriForFile(this, getApplicationContext().getPackageName() + ".provider", outputImage);
takePictureIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
takePictureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {
//7.0以下使用这种方式创建一个Uri
outImage = Uri.fromFile(outputImage);
}
//将Uri传递给系统相机
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, outImage);
startActivityForResult(takePictureIntent, TAKE_PHOTO);
}
}
}
回调方法中处理返回的数据:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case PHOTO_ALBUM:
if (resultCode == RESULT_OK) {
imageUri = data.getData();
//剪裁图片
Intent intent = CropUtils.invokeSystemCrop(imageUri, outImageUrl);
//启动裁剪
startActivityForResult(intent, CROP_PHOTO);
}
break;
case TAKE_PHOTO:
if (resultCode == RESULT_OK) {
//剪裁图片
Intent intent = CropUtils.invokeSystemCrop(outImage, outImageUrl);
// 启动裁剪
startActivityForResult(intent, CROP_PHOTO);
}
break;
case CROP_PHOTO:
if (resultCode == RESULT_OK) {
try {
outImage = CropUtils.getUriForFile(this, new File(outImageUrl));
bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(outImage));
bitmap = BitmapAndBase64.compressImage(bitmap);
sendPresenter();
} catch (IOException e) {
e.printStackTrace();
}
}
break;
default:
break;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case CAMERA_OK:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//这里已经获取到了摄像头的权限,想干嘛干嘛了可以
takePhoto();
} else {
//这里是拒绝给APP摄像头权限,给个提示什么的说明一下都可以。
Toast.makeText(SetUserHeadActivity.this, "请手动打开相机权限", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
最后,PopupWindow
package com.sharetronic.personal.popupwindow;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupWindow;
import android.widget.TextView;
import com.sharetronic.R;
/**
* Created by Administrator on 2018/9/12.
*/
public class SetUserHeadPopupwindow extends PopupWindow{
private TextView fromAlbum;
private TextView takePhoto;
private TextView cancel;
public SetUserHeadPopupwindow(Context context, View.OnClickListener onClickListener) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final View menuView = inflater.inflate(R.layout.setuserhead_popupwindow, null);
fromAlbum = menuView.findViewById(R.id.tv_setuserhead_from_album);
takePhoto = menuView.findViewById(R.id.tv_setuserhead_take_photo);
cancel = menuView.findViewById(R.id.tv_setuserhead_cancel);
fromAlbum.setOnClickListener(onClickListener);
takePhoto.setOnClickListener(onClickListener);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
}
});
//设置SelectPicPopupWindow的View
this.setContentView(menuView);
//设置SelectPicPopupWindow弹出窗体的宽
this.setWidth(ViewGroup.LayoutParams.FILL_PARENT);
//设置SelectPicPopupWindow弹出窗体的高
this.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
//设置SelectPicPopupWindow弹出窗体可点击
this.setFocusable(true);
//设置SelectPicPopupWindow弹出窗体动画效果
// this.setAnimationStyle(R.style.PopupwindowDialogAnimation);
//实例化一个ColorDrawable颜色为半透明
ColorDrawable dw = new ColorDrawable(0xb0000000);
//设置SelectPicPopupWindow弹出窗体的背景
this.setBackgroundDrawable(dw);
menuView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int top = menuView.findViewById(R.id.ll_setuserhead_userhead).getTop();
int y = (int) event.getY();
if (event.getAction() == MotionEvent.ACTION_UP) {
if (y < top) {
dismiss();
}
}
return true;
}
});
}
}
文章参考于:https://blog.csdn.net/runwx/article/details/75020865