前言
在Android7.0之前,第三方的应用可以访问我们自身应用的私有路径,比如通过Intent分享一张图片到第三方。Android7.0之后,当我们隐式启动其他应用时,使用File文件传递file://这样的URI私有目录,会导致对方应用访问不到这个路径从而触发FileUriExposedException异常,这是官方出于保护用户隐私的考虑,第三方应用是不可以随便访问我们APP的私有路径的。当我们应用打开另一个应用比如系统相机,对该文件的访问权应该是我们的应用程序而不是系统相机的。对文件进行的每个操作都应该通过我们的应用程序完成,而不是由系统相机完成。
官方说明
关于7.0之后的一些变更,具体可以参考地址(需科学上网):https://developer.android.google.cn/about/versions/nougat/android-7.0-changes.html


操作
今天就用一个启动系统相机的例子来演示如何避免这个问题。
1.权限
既然是拍照那肯定需要写入权限,照片是要保存到本地的。这里特别说明调用系统相机是不需要拍照权限的。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
2.Android7.0之前的调用方式
private void openCamera() {
String imageDir = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "images" + File.separator;
String imageName = System.currentTimeMillis() + ".jpg";
imagePath = imageDir + imageName;
File file = new File(imageDir, imageName);
if (!file.getParentFile().exists()) {
Log.e("yzt", "创建文件夹成功>>>" + file.getParentFile().mkdirs());
}
uri = Uri.fromFile(file);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, REQUEST_CODE_CAMERA);
}
3.Android7.0之后的调用方式
首先要在res之下新建一个xml文件夹,在其中再建立一个file_path.xml文件,这个文件的作用是为FileProvider提供可以暴露的路径,一旦一个路径在文件中被声明,那么就可以被FileProvider提供。这里的path就是共享的图片路径,name代表使用这个字段去访问真实的文件路径。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="Camera"
path="images/" />
</paths>
<paths>标签下有多种path可供使用,具体可参考地址(需科学上网):
https://developer.android.google.cn/reference/android/support/v4/content/FileProvider.html
然后在AndroidManifest.xml中声明一个provider组件(因为FileProvider是Android四大组件之一的ContentProvider的子类,因此需要在清单文件中声明),其中android:authorities可以随意填写,<meta-data>中的android:resource填写的就是刚才创建的file_path.xml。
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.yuzhentao.demo.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path" />
</provider>
这里与7.0之前最大的不同就在于Uri的获取方式,是通过FileProvider.getUriForFile()这个方法来获取的,其中第二个参数就是上面我们在<provider>中定义的 android:authorities,这里切记要填写相同。这里打印Uri可以看到Uri为content://com.yuzhentao.demo.fileprovider/Camera/XXXXX.jpg
private void openCamera_N() {
String imageDir = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "images" + File.separator;
String imageName = System.currentTimeMillis() + ".jpg";
imagePath = imageDir + imageName;
File file = new File(imageDir, imageName);
if (!file.getParentFile().exists()) {
Log.e("yzt", "创建文件夹成功>>>" + file.getParentFile().mkdirs());
}
uri = FileProvider.getUriForFile(context, "com.yuzhentao.demo.fileprovider", file);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, REQUEST_CODE_CAMERA);
}
4.使用图片
我们在onActivityResult()中来进行回调,因为ImageView.setImageBitmap()对图片大小有要求,过大的图片使用这个方法不能展示出图片,但是保存到存储中我们又希望是没有压缩的图片,所以这里图片的展示和图片的保存使用了2个Bitmap。这里还使用到了一个ExifInterfaceUtil来获取图片的角度,它是一个照片旋转角度获取工具,主要使用到ExifInterface这个类,通常情况,调用照相机拍照之后产生的图片默认旋转角度为0,此信息可以通过读取图片的EXIF信息来获取到。对于某些手机拍照之后旋转角度被改变了(我也不知道为什么他们要这么做,特别是三星手机),这时我们可以通过android.graphics.Matrix将照片角度在旋转回去即可,然后再进行显示和保存。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_CODE_CAMERA) {
if (uri == null || TextUtils.isEmpty(uri.getPath())) {
return;
}
Bitmap showBitmap = BitmapUtil.decodeFile(context, imagePath);
if (BitmapUtil.isUseful(showBitmap) && !TextUtils.isEmpty(imagePath)) {
int imageDegree = ExifInterfaceUtil.getDegree(imagePath);
if (BitmapUtil.isUseful(showBitmap) && imageDegree > 0) {
showBitmap = BitmapUtil.rotateBitmap(showBitmap, imageDegree, false);
}
}
((AppCompatImageView) findViewById(R.id.iv)).setImageBitmap(showBitmap);
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
if (BitmapUtil.isUseful(bitmap) && !TextUtils.isEmpty(imagePath)) {
int imageDegree = ExifInterfaceUtil.getDegree(imagePath);
if (BitmapUtil.isUseful(bitmap) && imageDegree > 0) {
bitmap = BitmapUtil.rotateBitmap(bitmap, imageDegree, false);
}
BitmapUtil.saveImage(context, imagePath, bitmap);
}
}
}
}
BitmapUtil
public class BitmapUtil {
public static Bitmap decodeFile(Context context, String path) {
Bitmap bitmap;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
options.inSampleSize = calculateInSampleSize(options, DimensionUtil.getWidthInPx(context), DimensionUtil.getHeightInPx(context));
options.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeFile(path, options);
return bitmap;
}
private static int calculateInSampleSize(BitmapFactory.Options options, float reqWidth, float reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
float maxPixels = reqWidth * reqHeight * 1.5f;
float realPixels = width * height;
if (maxPixels <= 0) {
return 1;
}
try {
if (realPixels <= maxPixels) {//如果图片尺寸小于最大尺寸,则直接读取
return 1;
} else {
int scale = 2;
while (realPixels / (scale * scale) > maxPixels) {
scale *= 2;
}
return scale;
}
} catch (Exception e) {
return 1;
}
}
public static void saveImage(Context context, String imagePath, Bitmap bitmap) {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return;
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(imagePath);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
scanFile(context, imagePath);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.flush();
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static Bitmap rotateBitmap(Bitmap bitmap, float degrees, boolean isRecycle) {
if (degrees == 0 || null == bitmap) {
return bitmap;
}
Matrix matrix = new Matrix();
matrix.setRotate(degrees, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
Bitmap bmp = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
if (isRecycle) {
bitmap.recycle();
}
return bmp;
}
public static void scanFile(Context context, String filePath) {
Intent scanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
scanIntent.setData(Uri.fromFile(new File(filePath)));
context.sendBroadcast(scanIntent);
}
public static boolean isUseful(Bitmap bitmap) {
return bitmap != null
&& !bitmap.isRecycled()
&& bitmap.getWidth() > 0
&& bitmap.getHeight() > 0
&& bitmap.getConfig() != null;
}
}
ExifInterfaceUtil,使用这个工具类先要依赖exifinterface这个库
implementation "com.android.support:exifinterface:28.0.0"
public class ExifInterfaceUtil {
/**
* 解决调用系统相机照片会自动旋转的问题
*/
public static int getDegree(String imagePath) {
int imageDegree = 0;
try {
ExifInterface exif = new ExifInterface(imagePath);
int ori = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
switch (ori) {
case ExifInterface.ORIENTATION_ROTATE_90:
imageDegree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
imageDegree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
imageDegree = 270;
break;
default:
imageDegree = 0;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return imageDegree;
}
}
结尾
以上大致是解决FileUriExposedException异常的一个流程,也有一些地方可以优化下,比如可以通过判断版本号来生成不同的Uri,这样就能兼容不同的Android版本。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(context, "com.yuzhentao.customview.fileprovider", file);
} else {
uri = Uri.fromFile(file);
}
网友评论