Android 7.0 FileUriExposedException 的处理

前言
  • 最近在做手机权限适配,在做到使用照相机权限的时候遇到了一个问题 FileUriExposedException ,一开始百思不得其解在AndroidN 之前没有问题,但是在N之后就会爆出这个问题,在查看了官方AndroidN新特性的说明后发现了问题。当然官方给了解决方案:官方给出的解决方式是通过 FileProvider 来为所共享的文件 Uri 添加临时权限。
照相机权限处理
  • 调用照相机的时候需要将一个URI传到照相机的应用里,用来作为图片的存储路径,这就违背了Android N的要求,如上图所说禁止应用私有文件对外公开(Android手机越来越看中安全挺好的,适配挺麻烦),所以需要做些处理
  • 之前的照相机的调用
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            
                uri = Uri.fromFile(mFile);
              intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
                startActivityForResult(intent, PHOTOHRAPH);
  • 7.0适配,URI的获取方式不再是Uri.fromFile(mFile),而是需要通过FileProvider获取 uri = FileProvider.getUriForFile(this, “包名.fileprovider”, mFile),而且同时需要为他赋予临时权限 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);不然会报错,程序会崩溃
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                    //7.0以上 的拍照文件必须在storage/emulated/0/Android/data/包名/files/pictures文件夹
                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    uri = FileProvider.getUriForFile(this, "cn.com.jsj.GCTravelTools.fileprovider", mFile);
                intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
                startActivityForResult(intent, PHOTOHRAPH);
系统图片裁剪
  • 因为是做头像处理,拍照获取图片后需要裁剪,而本人不太喜欢用第三方,所以就用系统的裁剪功能,然后也遇到了坑,其实都是一个问题
try {
            //剪切后的图片存储位置
            // 调用系统中自带的图片剪裁
            Intent intent = new Intent("com.android.camera.action.CROP");
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
            }
            resultUri = Uri.fromFile(mCutResultFile);
            // 调用系统中自带的图片剪裁
            intent.setDataAndType(uri, IMAGE_UNSPECIFIED);
            intent.putExtra("crop", "true");
            intent.putExtra("scale", true);
            //裁剪框的比例
            intent.putExtra("aspectX", 1);
            intent.putExtra("aspectY", 1);
            //输出图片大小
            intent.putExtra("outputX", 300);
            intent.putExtra("outputY", 300);
            intent.putExtra("outputFormat", Bitmap.CompressFormat.PNG.toString());
            intent.putExtra("noFaceDetection", true);
            intent.putExtra("return-data", false);
            //设置裁剪照片完成后图片的存放地址
            intent.putExtra(MediaStore.EXTRA_OUTPUT, resultUri);
            startActivityForResult(intent, PHOTORESOULT);
        } catch (Exception e) {
            e.printStackTrace();
        }
  • 一些注意的问题:裁剪后的图片通过Intent的putExtra(“return-data”,true)方法进行传递,miui系统问题就出在这里,return-data的方式只适用于小图,miui系统默认的裁剪图片可能裁剪得过大,或对return-data分配的资源不足,造成return-data失败。
    解决思路是:裁剪后,将裁剪的图片保存在Uri中,在onActivityResult()方法中,再提取对应的Uri图片转换为Bitmap使用。
    其实大家直观也能感觉出来,Intent主要用于不同Activity之间通信,并不适用于传递图片之类的大数据。于是当A生成一个大数据要传递给B,往往不是通过Intent直接传递,而是在A生成数据的时候将数据保存到C,B再去调用C,C相当于一个转换的中间件。所以在这里也会遇到上述相同的问题,因此调用的时候需要赋予临时权限,可参考此博客
具体使用流程
  • 在 标签下添加 FileProvider 节点
<provider android:name="android.support.v4.content.FileProvider"
            android:authorities="包名.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

android:authority 属性指定要用于 FileProvider 生成的 content URI 的 URI 权限,这里推荐使用 包名.fileprovider 以确保其唯一性。
的 子元素指向一个 XML 文件,用于指定要共享的目录。

  • 在 res/xml 目录下创建文件 file_paths.xml 内容如下:
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="picture" //随便起,不影响
        path="." />
</paths>

关于这里面的属性可以做个简单说明

Android 14 相机 文件 相册权限 安卓手机app相机权限被禁_照相

做好前期准备工作后,上完整的代码
private final String IMAGE_UNSPECIFIED = "image/*";
    private final int PHOTOHRAPH = 201;// 拍照
    private final int PHOTOZOOM = 202; // 缩放
    private final int PHOTORESOULT = 203;// 结果
    private File mFile; //拍照图片存储路径
    private File mCutResultFile;  //裁剪图片存储路径
    private Uri uri = null;
    String photoPic = System.currentTimeMillis() + "temp1.png";
    String cutPic = System.currentTimeMillis() + "cutResult.png";

    private void initIcon() {
        File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        mFile = new File(storageDir + File.separator + photoPic);
        try {
            if (mFile.exists()) {
                mFile.delete();
            }
            mFile.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }

        mCutResultFile = new File(storageDir + File.separator + cutPic);
        try {
            if (mCutResultFile.exists()) {
                mCutResultFile.delete();
            }
            mCutResultFile.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 图片上传
     *
     * @param i
     */
    private void uploadPictures(int i) {
        try {
        *
        *
        *
            if (i == R.id.btn_common_open_camera) {
                Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    //7.0以上 的拍照文件必须在storage/emulated/0/Android/data/包名/files/pictures文件夹
                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    uri = FileProvider.getUriForFile(this, "cn.com.jsj.GCTravelTools.fileprovider", mFile);
                } else {
                    uri = Uri.fromFile(mFile);
                }
                intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
                startActivityForResult(intent, PHOTOHRAPH);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        // 拍照
        if (requestCode == PHOTOHRAPH) {
            startPhotoZoom(uri);
        }
        if (data == null)
            return;
        if (requestCode == PHOTORESOULT) {
            //节省内存的办法
            Bitmap mBitmap = SaImageUtils.getScaleBitmap(this, resultUri.getPath());
            portraitBitmap = mBitmap;
            if (null != mBitmap) {
                mFltTicketPic = BitmapToBytes(mBitmap);
                savePortraitInfo();
            }
        }
    }

    Uri resultUri;

    /**
     * 图片剪裁功能
     */
    private void startPhotoZoom(Uri uri) {
        try {
            //剪切后的图片存储位置
            // 调用系统中自带的图片剪裁
            Intent intent = new Intent("com.android.camera.action.CROP");
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
            }
            resultUri = Uri.fromFile(mCutResultFile);
            // 调用系统中自带的图片剪裁
            intent.setDataAndType(uri, IMAGE_UNSPECIFIED);
            intent.putExtra("crop", "true");
            intent.putExtra("scale", true);
            //裁剪框的比例
            intent.putExtra("aspectX", 1);
            intent.putExtra("aspectY", 1);
            //输出图片大小
            intent.putExtra("outputX", 300);
            intent.putExtra("outputY", 300);
            intent.putExtra("outputFormat", Bitmap.CompressFormat.PNG.toString());
            intent.putExtra("noFaceDetection", true);
            intent.putExtra("return-data", false);
            //设置裁剪照片完成后图片的存放地址
            intent.putExtra(MediaStore.EXTRA_OUTPUT, resultUri);
            startActivityForResult(intent, PHOTORESOULT);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

另附

/**
     * 描述:压缩文件图片位图,图片长宽不处理
     *
     * @param ctx
     * @param filePath
     * @return
     */
    public static Bitmap getScaleBitmap(Context ctx, String filePath) {
        BitmapFactory.Options opt = new BitmapFactory.Options();
        opt.inJustDecodeBounds = true;
        Bitmap bmp = BitmapFactory.decodeFile(filePath, opt);

        int bmpWidth = opt.outWidth;
        int bmpHeght = opt.outHeight;

        WindowManager windowManager = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
        Display display = windowManager.getDefaultDisplay();
        int screenWidth = display.getWidth();
        int screenHeight = display.getHeight();

        opt.inSampleSize = 1;
        if (bmpWidth > bmpHeght) {
            if (bmpWidth > screenWidth)
                opt.inSampleSize = bmpWidth / screenWidth;
        } else {
            if (bmpHeght > screenHeight)
                opt.inSampleSize = bmpHeght / screenHeight;
        }
        opt.inJustDecodeBounds = false;

        bmp = BitmapFactory.decodeFile(filePath, opt);
        return bmp;
    }
注意

除了上面这个问题,在 API Level 24(Android 7.0)之前开发的分享图文、浏览编辑本地图片、共享互传文件等功能如果没有使用 FileProvider 来生成 URI 的话,在 Android 7.0 上就必须做这种适配了,所以平时建议大家多关注 Android 新的 API ,尽早替换已被官方废弃的 API ,实际上 FileProvider 在 API Level 22(Android 5.1) 已经添加了。