Android|Android 7.0适配 -- FileProvider 拍照、选择相册、裁切图片, 小米机型适配

Demo下载地址:https://pan.baidu.com/s/1dnaugm
需求:
最近把APP的TargetSdk从21提高至25后,测试时,
在Android7.0以上的系统上,爆出了一些异常。
在个别小米等机型也存在一些异常。
问题分析:

  1. 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
  1. 小米手机 Unable to load resource 0x00000000 from pkg=com.android.systemui 异常,
    主要原因:裁切图片时,直接通过intent返回图片数据导致。
解决方案:
  1. 向其他APP传递uri数据 的异常,我们使用FileProvider 把 file:// URI转为content:// URI再传递即可解决。
    如果只是在自己APP中单独使用 file:// URI是没有问题的。
  2. 小米手机 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); } }

  1. 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 =https://www.it610.com/article/= 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小米手机

Android|Android 7.0适配 -- FileProvider 拍照、选择相册、裁切图片, 小米机型适配
文章图片
APP启动后的页面.jpg
demo中使用了拍照、相册等功能,使用前需要先去动态授权,动态授权的代码请参考:
https://www.jianshu.com/p/8e37e9cf20a5
Android|Android 7.0适配 -- FileProvider 拍照、选择相册、裁切图片, 小米机型适配
文章图片
拍照、选择相册图片后的 图片裁剪页面 .jpg Android|Android 7.0适配 -- FileProvider 拍照、选择相册、裁切图片, 小米机型适配
文章图片
显示裁切后的图片.jpg 【Android|Android 7.0适配 -- FileProvider 拍照、选择相册、裁切图片, 小米机型适配】Demo下载地址:https://pan.baidu.com/s/1dnaugm

    推荐阅读