最近将项目的targetSdkVersion升级到了26,发现调用系统相机的时候报了下面这个错误:
android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/blog.csdn.net.mchenys/cache/output_image.jpg exposed beyond app through ClipData.Item.getUri()
经过排查发现是 imageUri = Uri.fromFile(outputImage);这段代码报了错误.
同样的代码,为什么sdk版本没升级前是运行正常的,升级后就报错了呢,经过一番资料查询发现从Android 7.0开始,一个应用提供自身文件给其它应用使用时,如果给出一个file://格式的URI的话,应用会抛出FileUriExposedException。这是由于谷歌认为目标app可能不具有文件权限,会造成潜在的问题。
那么怎么解决呢?
这就需要用到FileProvider这个东西了,具体使用步骤如下:
1.在AndroidManifest.xml中添加如下代码
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="blog.csdn.net.mchenys.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
provider>
其中authorities可以随意配置,通常为了确保唯一,都是包名+.fileprovider的格式.同时注意下项目中使用的时候也是要跟这里匹配就可以了.
2.在在res目录下新建一个xml文件夹,并且新建一个file_paths的xml文件
<paths>
<external-path name="external_files" path="."/>
paths>
更多的节点的含义,可以参考下面这个表:
经过上面这样的配置,其实就是将某个路径通过别名的形式标记起来了,例如external_files在本例中就表示的是sd卡的根目录.
3.将项目中的imageUri = Uri.fromFile(outputImage);这段代码修改成下面方式:
if (Build.VERSION.SDK_INT >= 24) {
String authority = this.getPackageName() + ".fileprovider";
imageUri = FileProvider.getUriForFile(this, authority, outputImage);
} else {
imageUri = Uri.fromFile(outputImage);
}
4.将 uri.getPath()这段代码做下调整.
当调用系统相册拍照完后,相片的路径信息已经保存到了我们指定的imageUri .如果我们直接通过imageUri .getPath()来获取该图片的路径的话,你会发现根本无法使用,通过log将imageUri 打印,同时将getScheme()、getPath()、getAuthority()得到的内容也打印,结果如下所示:
Uri:
content://blog.csdn.net.mchenys.fileprovider/external_files/Android/data/blog.csdn.net.mchenys/cache/output_image.jpg
Scheme:content
Path:
/external_files/Android/data/blog.csdn.net.mchenys/cache/output_image.jpg
Authority:blog.csdn.net.mchenys.fileprovider
观察Path发现多了个external_files,敏锐的你肯定知道了原因,这个其实就是我们在步骤2中的file_paths.xml里定义的external-path节点的name.所以这就是为什么说imageUri .getPath()拿到的路径是用不了的,因为sd卡中根本就不存在external_files这个文件夹,那么如何解决呢?
方法也很简单,当拿到path之后,通过下面的方式替换一下就可以了
String path = imageUri.getPath();
if(path.contains("external_files")){
path = path.replaceAll("/external_files",Environment.getExternalStorageDirectory().
getAbsolutePath());
}
//替换后的path就是/storage/emulated/0/Android/data/blog.csdn.net.mchenys/cache/output_image.jpg,这样就可以正常访问了.
Ok,上面的插曲讲完之后,通过一个demo来实例下在android7.0及以上的调用系统相机的具体操作:
activity_main.xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="blog.csdn.net.mchenys.MainActivity">
<Button
android:text="takephoto"
android:onClick="takePhoto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
LinearLayout>
为了兼容android4.4之前的系统,需要在AndroidManifest.xml中添加访问SD卡的权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
MainActivity.java
package blog.csdn.net.mchenys;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.annotation.RequiresApi;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ImageView;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
public static final int TAKE_PHOTO = 1;
private ImageView picture;
private Uri imageUri;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
picture = findViewById(R.id.picture);
}
@RequiresApi(api = Build.VERSION_CODES.FROYO)
public void takePhoto(View view) {
/*
getExternalCacheDir访问的应用的私有目录(/sdcard/Android/data//cache)
因此不需要动态权限申请.
*/
File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
try {
if (outputImage.exists()) {
outputImage.delete();
outputImage.createNewFile();
}
} catch (IOException e) {
e.printStackTrace();
}
//兼容7.0的方式获取uri
if (Build.VERSION.SDK_INT >= 24) {
imageUri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", outputImage);
} else {
imageUri = Uri.fromFile(outputImage);
}
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
switch (requestCode) {
case TAKE_PHOTO:
try {
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
picture.setImageBitmap(bitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
break;
}
}
}
}
最后,别忘了配置FileProvider,按照上文介绍的方式配置就可以了.
看到这里,相信大伙都以为就只有这一种解决方式了,哈,下面介绍一种更吊的方式,可以直接屏蔽掉FileUriExposedException.
只需要在Activity的onCreate方法中加入下面的代码即可.
//屏蔽7.0中使用 Uri.fromFile爆出的FileUriExposureException
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
if (Build.VERSION.SDK_INT >=24) {
builder.detectFileUriExposure();
}
然后剩下的事情就是动态权限申请了,因为读取和写入sd卡在android6.0之后需要动态申请权限,当然如果使用的是私有目录的话也可以不用申请权限.
demo如下,包含调用系统拍照和裁剪的功能
package blog.csdn.net.mchenys;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.StrictMode;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ImageView;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* 该类展示,屏蔽FileProvider.getUriForFile方式获取uri的操作
* Created by mChenys on 2018/4/25.
*/
public class MainActivity2 extends AppCompatActivity {
private ImageView picture;
private Uri imageUri;
private Uri cropImgUri;
public static final int TAKE_PHOTO = 1;
public static final int CROP_PHOTO = 2;
public static final int GET_PERMISSION = 3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//屏蔽7.0中使用 Uri.fromFile爆出的FileUriExposureException
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
if (Build.VERSION.SDK_INT >= 24) {
builder.detectFileUriExposure();
}
picture = findViewById(R.id.picture);
}
public void takePhoto(View view) {
/* if (Build.VERSION.SDK_INT >= 23) {
boolean hasPermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
if (hasPermission ) {
openCamera();
} else {
showDialog("拍照需要获取存储权限", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(MainActivity2.this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, GET_PERMISSION);
}
});
}
} else {
openCamera();
}*/
//如果操作的是私有目录,可以不用申请权限
openCamera();
}
private void openCamera() {
// File outputImage = new File(Environment.getExternalStorageDirectory(), "output_image.jpg");
File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
try {
if (outputImage.exists()) {
outputImage.delete();
outputImage.createNewFile();
}
} catch (IOException e) {
e.printStackTrace();
}
imageUri = Uri.fromFile(outputImage);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
switch (requestCode) {
case TAKE_PHOTO: //处理拍照返回结果
startPhotoCrop();
break;
case CROP_PHOTO://处理裁剪返回结果
try {
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(cropImgUri));
picture.setImageBitmap(bitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
break;
}
}
}
/**
* 开启裁剪相片
*/
public void startPhotoCrop() {
//创建file文件,用于存储剪裁后的照片
// File cropImage = new File(Environment.getExternalStorageDirectory(), "crop_image.jpg");
File cropImage = new File(getExternalCacheDir(), "crop_image.jpg");
try {
if (cropImage.exists()) {
cropImage.delete();
}
cropImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
cropImgUri = Uri.fromFile(cropImage);
Intent intent = new Intent("com.android.camera.action.CROP");
//设置源地址uri
intent.setDataAndType(imageUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 200);
intent.putExtra("outputY", 200);
intent.putExtra("scale", true);
//设置目的地址uri
intent.putExtra(MediaStore.EXTRA_OUTPUT, cropImgUri);
//设置图片格式
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("return-data", false);//data不需要返回,避免图片太大异常
intent.putExtra("noFaceDetection", true); // no face detection
startActivityForResult(intent, CROP_PHOTO);
}
//弹窗提示
private void showDialog(String text, DialogInterface.OnClickListener listener) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("权限申请")
.setMessage(text)
.setPositiveButton("确定", listener)
.show();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == GET_PERMISSION &&
grantResults[0] == PackageManager.PERMISSION_GRANTED ) {
openCamera();
}
}
}