Demo下载地址:https://pan.baidu.com/s/1dnaugm
需求:
最近把APP的TargetSdk从21提高至25后,测试时,
在Android7.0以上的系统上,爆出了一些异常。
在个别小米等机型也存在一些异常。
问题分析:
- FileUriExposedException文件URI暴露异常
主要原因:不符合Android7.0安全要求;
谷歌官方的解释:
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。
要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。
如需了解有关权限和共享文件的详细信息,请参阅共享文件。
https://developer.android.com/about/versions/nougat/android-7.0-changes.html#accessibility
- 小米手机 Unable to load resource 0x00000000 from pkg=com.android.systemui 异常,
主要原因:裁切图片时,直接通过intent返回图片数据导致。
解决方案:
- 向其他APP传递uri数据 的异常,我们使用FileProvider 把 file:// URI转为content:// URI再传递即可解决。
如果只是在自己APP中单独使用 file:// URI是没有问题的。 - 小米手机 Unable to load resource 0x00000000 from pkg=com.android.systemui 的异常,
我们裁剪完图片,不直接返回图片数据,而是返回指向裁剪后图片的uri即可。
我们项目中,传递uri的应用场景主要是:设置用户头像(拍照、相册选取、裁切),拍照等功能;
在使用前,应该先将公共的内容,抽取到一个独立的模块中,以便于将来维护和扩展:
代码实现步骤:
1.封装重复的内容:
a. 封装FileProvider类,提供转换 file:// URI 为 content:// URI的功能
b. AndroidManifest.xml中注册FileProvider(ContentProvider的子类,4大组件之一,需要注册)
c. res中新建@xml/file_paths文件(注册FileProvider时用到)
d. 封装拍照、打开相册、裁切等系统程序的调用
2 UI调用封装好的代码
具体实现:
1.封装重复的内容:
a. FileProviderUtils类,封装FileProvider类,提供转换 file:// URI 为 content:// URI的功能
package iwangzhe.paizhaocaiqie.android7.uri;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.support.v4.content.FileProvider;
import java.io.File;
/**
* 类:FileProviderUtils
* 从APP向外共享的文件URI时,必须使用该类进行适配,否则在7.0以上系统,会报错:FileUriExposedException(文件Uri暴露异常)
* 作者: qxc
* 日期:2018/2/23.
*/
public class FileProviderUtils {
/**
* 从文件获得URI
* @param activity 上下文
* @param file 文件
* @return 文件对应的URI
*/
public static Uri uriFromFile(Activity activity, File file) {
Uri fileUri;
//7.0以上进行适配
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String p = activity.getPackageName() + ".FileProvider";
fileUri = FileProvider.getUriForFile(
activity,
p,
file);
} else {
fileUri = Uri.fromFile(file);
}
return fileUri;
}
/**
* 设置Intent的data和类型,并赋予目标程序临时的URI读写权限
* @param activity 上下文
* @param intent 意图
* @param type 类型
* @param file 文件
* @param writeAble 是否赋予可写URI的权限
*/
public static void setIntentDataAndType(Activity activity,
Intent intent,
String type,
File file,
boolean writeAble) {
//7.0以上进行适配
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setDataAndType(uriFromFile(activity, file), type);
//临时赋予读写Uri的权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (writeAble) {
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
} else {
intent.setDataAndType(Uri.fromFile(file), type);
}
}
/**
* 设置Intent的data和类型,并赋予目标程序临时的URI读写权限
* @param context 上下文
* @param intent 意图
* @param type 类型
* @param fileUri 文件uri
* @param writeAble 是否赋予可写URI的权限
*/
public static void setIntentDataAndType(Context context,
Intent intent,
String type,
Uri fileUri,
boolean writeAble) {
//7.0以上进行适配
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setDataAndType(fileUri, type);
//临时赋予读写Uri的权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (writeAble) {
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
} else {
intent.setDataAndType(fileUri, type);
}
}
}
b. AndroidManifest.xml中注册FileProvider(ContentProvider的子类,4大组件之一,需要注册)
c. res中新建@xml/file_paths文件(注册FileProvider时用到)
d. SystemProgramUtils:对于拍照、打开相册、裁切等系统程序的调用进行封装
package iwangzhe.paizhaocaiqie.android7.uri;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.provider.MediaStore;
import java.io.File;
/**
* 类:SystemProgramUtils 系统程序适配
* 1. 拍照
* 2. 相册
* 3. 裁切
* 作者: qxc
* 日期:2018/2/23.
*/
public class SystemProgramUtils {
public static final int REQUEST_CODE_PAIZHAO = 1;
public static final int REQUEST_CODE_ZHAOPIAN = 2;
public static final int REQUEST_CODE_CAIQIE = 3;
public static void paizhao(Activity activity, File outputFile){
Intent intent = new Intent();
intent.setAction("android.media.action.IMAGE_CAPTURE");
intent.addCategory("android.intent.category.DEFAULT");
Uri uri = FileProviderUtils.uriFromFile(activity, outputFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
activity.startActivityForResult(intent, REQUEST_CODE_PAIZHAO);
}
public static void zhaopian(Activity activity){
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction("android.intent.action.PICK");
intent.addCategory("android.intent.category.DEFAULT");
activity.startActivityForResult(intent, REQUEST_CODE_ZHAOPIAN);
}
public static void Caiqie(Activity activity, Uri uri, File outputFile) {
Intent intent = new Intent("com.android.camera.action.CROP");
FileProviderUtils.setIntentDataAndType(activity, intent, "image/*", uri, true);
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 300);
intent.putExtra("outputY", 300);
//return-data为true时,直接返回bitmap,可能会很占内存,不建议,小米等个别机型会出异常!!!
//所以适配小米等个别机型,裁切后的图片,不能直接使用data返回,应使用uri指向
//裁切后保存的URI,不属于我们向外共享的,所以可以使用fill://类型的URI
Uri outputUri = Uri.fromFile(outputFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
intent.putExtra("return-data", false);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true);
activity.startActivityForResult(intent, REQUEST_CODE_CAIQIE);
}
}
- UI调用封装好的代码
package iwangzhe.paizhaocaiqie;
import android.Manifest;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;
import java.io.File;
import iwangzhe.paizhaocaiqie.android7.uri.FileProviderUtils;
import iwangzhe.paizhaocaiqie.android7.uri.SystemProgramUtils;
import iwangzhe.paizhaocaiqie.permission.PermissionUtils;
import iwangzhe.paizhaocaiqie.permission.request.IRequestPermissions;
import iwangzhe.paizhaocaiqie.permission.request.RequestPermissions;
import iwangzhe.paizhaocaiqie.permission.requestresult.IRequestPermissionsResult;
import iwangzhe.paizhaocaiqie.permission.requestresult.RequestPermissionsResultSetApp;
public class MainActivity extends AppCompatActivity {
Button btnPaizhao;
Button btnXiangce;
ImageView ivTupian;
IRequestPermissions requestPermissions = RequestPermissions.getInstance();//动态权限请求
IRequestPermissionsResult requestPermissionsResult = RequestPermissionsResultSetApp.getInstance();//动态权限请求结果处理
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initEvent();
}
//初始化控件
private void initView(){
btnPaizhao = (Button) findViewById(R.id.paizhao);
btnXiangce = (Button) findViewById(R.id.xiangce);
ivTupian = (ImageView) findViewById(R.id.tupian);
}
//初始化事件
private void initEvent(){
//拍照
btnPaizhao.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(!requestPermissions()){
return;
}
SystemProgramUtils.paizhao(MainActivity.this, new File("/mnt/sdcard/tupian.jpg"));
}
});
//相册
btnXiangce.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(!requestPermissions()){
return;
}
SystemProgramUtils.zhaopian(MainActivity.this);
}
});
}
//请求权限
private boolean requestPermissions(){
//需要请求的权限
String[] permissions = {Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.CAMERA};
//开始请求权限
return requestPermissions.requestPermissions(
this,
permissions,
PermissionUtils.ResultCode1);
}
//用户授权操作结果(可能授权了,也可能未授权)
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
//用户给APP授权的结果
//判断grantResults是否已全部授权,如果是,执行相应操作,如果否,提醒开启权限
if(requestPermissionsResult.doRequestPermissionsResult(this, permissions, grantResults)){
//请求的权限全部授权成功,此处可以做自己想做的事了
//输出授权结果
Toast.makeText(MainActivity.this,"授权成功,请重新点击刚才的操作!",Toast.LENGTH_LONG).show();
}else{
//输出授权结果
Toast.makeText(MainActivity.this,"请给APP授权,否则功能无法正常使用!",Toast.LENGTH_LONG).show();
}
}
//拍照、相册、图片裁切结果回调
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK) {
return;
}
Uri filtUri;
File outputFile = new File("/mnt/sdcard/tupian_out.jpg");//裁切后输出的图片
switch (requestCode) {
case SystemProgramUtils.REQUEST_CODE_PAIZHAO:
//拍照完成,进行图片裁切
File file = new File("/mnt/sdcard/tupian.jpg");
filtUri = FileProviderUtils.uriFromFile(MainActivity.this, file);
SystemProgramUtils.Caiqie(MainActivity.this, filtUri, outputFile);
break;
case SystemProgramUtils.REQUEST_CODE_ZHAOPIAN:
//相册选择图片完毕,进行图片裁切
if (data == null || data.getData()==null) {
return;
}
filtUri = data.getData();
SystemProgramUtils.Caiqie(MainActivity.this, filtUri, outputFile);
break;
case SystemProgramUtils.REQUEST_CODE_CAIQIE:
//图片裁切完成,显示裁切后的图片
try {
Uri uri = Uri.fromFile(outputFile);
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
ivTupian.setImageBitmap(bitmap);
}catch (Exception ex){
ex.printStackTrace();
}
break;
}
}
}
效果图:
测试机型:7.1小米手机
demo中使用了拍照、相册等功能,使用前需要先去动态授权,动态授权的代码请参考:
https://www.jianshu.com/p/8e37e9cf20a5
Demo下载地址:https://pan.baidu.com/s/1dnaugm