Android 7.0及其以上系统拍照,打开相册,裁剪,报错: android.os.FileUriExposedException: file:///storage/emulated/0/.....

全部代码:点击下载

注意:如果你原先的应用的targetSdkVersion本来就小与27。那就拍照。什么都不修改。也不会崩溃。但是、一旦你修改了你的targetSdkVersion为27.或者28。那你的应用就会报出这些问题。。具体原因。请自行百度下targetSdkVersion的意义。

Android 7.0以上的系统。在拍照的时候。报错:

android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/XXX/files/avatar.jpg exposed beyond app through ClipData.Item.getUri()

在网上查一下就可以知道。这是Android7.0的“私有目录被限制访问”。具体的解释简书的一篇文章:

https://www.jianshu.com/p/2275bb552327

里面讲的很仔细。

然后我们知道问题的解决办法就是通过FileProvider.可是我们应该怎么用。在哪个地方使用。

A:FileProvider的使用步骤:

 1:在资源(res)目录下创建一个xml目录,然后创建一个名为"file_paths"的资源文件
  (res-->new -->Directory.然后输入xml。在xml上右击--》new--》XML--->ValuesXmL-->输入名字file_paths
  不过会跑到values下面去。。不慌我们剪切到xml下面即可)

xml里面的内容如下:


具体解释:借用上面链接文章里面的一段文字:

所以:我上面的路径应该是:/storage/emulated/0/Android/data/com.example.zongm.testapplication/  之所以这样写。是想要在应用删除的时候。在应用里面拍的照片也能够被删除掉。

2:在manifest清单文件中注册provider(放在application节点里面)

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.example.zongm.testapplication.provider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

解释:

android:authorities="com.example.zongm.testapplication.provider" 这个值可以随便写。我用的是appid。这个值决定了fileProVider生成的uri的路径。后面详细介绍
exported:要求必须为false,为true则会报安全异常。grantUriPermissions:true,表示授予 URI 临时访问权限。

然后我们就可以在代码中使用FileProvider了。

使用:

private Uri getImageUri() {
    if (isSdCardExist()) {
        photo_image = new SimpleDateFormat("yyyy_MMdd_hhmmss").format(new Date());
        File[] dirs = ContextCompat.getExternalFilesDirs(this, null);
        if (dirs != null && dirs.length > 0) {
            File dir = dirs[0];
            File file = new File(dir, photo_image);
            takePath = file.getAbsolutePath();
            Log.e("zmm", "图片的路径---》" + takePath);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                //FileProvider.getUriForFile();第一个参数是context.
                // 第二个值。比较关键、这个也就是我们在manifest里面的provider里面的
                //android:authorities="com.example.zongm.testapplication.provider"
                //因为我用的就是AppId.所以。这里就直接用BuildConfig.APPLICATION_ID了。
                //如果你的android:authorities="test.provider"。那这里第二个参数就应该是test.provider
                return FileProvider.getUriForFile(getApplicationContext(),
                    BuildConfig.APPLICATION_ID + ".provider", file);
            } else {
                return Uri.fromFile(file);
            }

        }
    }
    return Uri.EMPTY;
}

下面通过一个例子来看看我们如何使用。代码注释写的很清楚了应该:


public class Main3Activity extends AppCompatActivity {
    //打开相机的返回码
    private static final int CAMERA_REQUEST_CODE = 1;
    //选择图片的返回码
    private static final int IMAGE_REQUEST_CODE = 2;
    //剪切图片的返回码
    public static final int CROP_REREQUEST_CODE = 3;
    private ImageView iv;

    //相机
    public static final int REQUEST_CODE_PERMISSION_CAMERA = 100;

    public static final int REQUEST_CODE_PERMISSION_GALLERY = 101;

    //照片图片名
    private String photo_image;
    //截图图片名
    private String crop_image;

    //拍摄的图片的真实路径
    private String takePath;
    //拍摄的图片的虚拟路径
    private Uri imageUri;
    private Uri cropUri;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main3);
        iv = findViewById(R.id.iv);
    }

    /**
     * 拍照
     *
     * @param view
     */
    public void onClickTakePhoto(View view) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            checkPermission(REQUEST_CODE_PERMISSION_CAMERA);
            return;
        }
        openCamera();
    }

    /**
     * 打开系统的相机的时候。我们需要传入一个uri。该uri就是拍摄的照片的地址。
     * 也就是:cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, getImageUri());
     * 这里就用到了FileProvider
     */
    private void openCamera() {
        if (isSdCardExist()) {
            Intent cameraIntent = new Intent(
                "android.media.action.IMAGE_CAPTURE");

            photo_image = new SimpleDateFormat("yyyy_MMdd_hhmmss").format(new Date()) + ".jpg";
            imageUri = getImageUri(photo_image);
            //Log.e("zmm", "图片存储的uri---------->" + imageUri);
            cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT
                , imageUri);
            //添加这一句表示对目标应用临时授权该Uri所代表的文件
            cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            cameraIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
            startActivityForResult(cameraIntent, CAMERA_REQUEST_CODE);
        } else {
            Toast.makeText(this, "SD卡不存在", Toast.LENGTH_SHORT).show();
        }
    }


    /**
     * 打开图库
     * 不需要用FileProvider
     *
     * @param view
     */
    public void onClickOpenGallery(View view) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            checkPermission(REQUEST_CODE_PERMISSION_GALLERY);
            return;
        }
        openGallery();
    }

    private void openGallery() {
        Intent galleryIntent = new Intent(Intent.ACTION_GET_CONTENT);
        galleryIntent.addCategory(Intent.CATEGORY_OPENABLE);
        galleryIntent.setType("image/*");
        startActivityForResult(galleryIntent, IMAGE_REQUEST_CODE);
    }

    /**
     * @param path 原始图片的路径
     */
    public void cropPhoto(String path) {
        crop_image = new SimpleDateFormat("yyyy_MMdd_hhmmss").format(new Date()) + "_crop" +
            ".jpg";
        File cropFile = createFile(crop_image);
        File file = new File(path);


        Intent intent = new Intent("com.android.camera.action.CROP");
        //TODO:访问相册需要被限制,需要通过FileProvider创建一个content类型的Uri
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            //添加这一句表示对目标应用临时授权该Uri所代表的文件
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            //TODO:访问相册需要被限制,需要通过FileProvider创建一个content类型的Uri
            imageUri = FileProvider.getUriForFile(getApplicationContext(),
                BuildConfig.APPLICATION_ID + ".provider", file);
            cropUri = Uri.fromFile(cropFile);
              //TODO:cropUri 是裁剪以后的图片保存的地方。也就是我们要写入此Uri.故不需要用FileProvider
            //cropUri = FileProvider.getUriForFile(getApplicationContext(),
            //    BuildConfig.APPLICATION_ID + ".provider", cropFile);
        } else {
            imageUri = Uri.fromFile(file);
            cropUri = Uri.fromFile(cropFile);
        }

        intent.setDataAndType(imageUri, "image/*");
        intent.putExtra("crop", "true");
        //设置宽高比例
        intent.putExtra("aspectX", 1);
        intent.putExtra("aspectY", 1);
        //设置裁剪图片宽高
        intent.putExtra("outputX", 400);
        intent.putExtra("outputY", 400);
        intent.putExtra("scale", true);
        //裁剪成功以后保存的位置
        intent.putExtra(MediaStore.EXTRA_OUTPUT, cropUri);
        intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
        intent.putExtra("noFaceDetection", true);
        startActivityForResult(intent, CROP_REREQUEST_CODE);


    }


    /**
     * 获得一个uri。该uri就是将要拍摄的照片的uri
     *
     * @return
     */
    private Uri getImageUri(String name) {
        if (isSdCardExist()) {
            File file = createFile(name);
            if (file != null) {
                takePath = file.getAbsolutePath();
                Log.e("zmm", "图片的路径---》" + takePath);
                // 输出是/storage/emulated/0/Android/data/com.example.zongm.testapplication/files/2018_0713_111455.jpg
                // 根据这个path。拿到的Uri是:content://com.example.zongm.testapplication.provider/files_root/files/2018_0713_111455.jpg
                //我们可以看到真实路径:/Android/data/com.example.zongm.testapplication这一部分被files_root替代了
                //也就是我们在file_path里面写的<external-path
                //            name="files_root"
                //            path="Android/data/com.example.zongm.testapplication/" />
                //其中external-path代表的是 Environment.getExternalStorageDirectory() 也就是/storage/emulated/0
                //。。。。我说的有点乱。大家还是看那篇简书文章吧。:链接:https://www.jianshu.com/p/56b9fb319310
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    //FileProvider.getUriForFile();第一个参数是context.
                    // 第二个值。比较关键、这个也就是我们在manifest里面的provider里面的
                    //android:authorities="com.example.zongm.testapplication.provider"
                    //因为我用的就是AppId.所以。这里就直接用BuildConfig.APPLICATION_ID了。
                    //如果你的android:authorities="test.provider"。那这里第二个参数就应该是test.provider
                    return FileProvider.getUriForFile(getApplicationContext(),
                        BuildConfig.APPLICATION_ID + ".provider", file);
                } else {
                    return Uri.fromFile(file);
                }

            }
        }
        return Uri.EMPTY;
    }

    public File createFile(String name) {
        if (isSdCardExist()) {
            File[] dirs = ContextCompat.getExternalFilesDirs(this, null);
            if (dirs != null && dirs.length > 0) {
                File dir = dirs[0];
                return new File(dir, name);
            }
        }

        return null;
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                case CAMERA_REQUEST_CODE://拍照成功并且返回
                    //注意这里。可以直接用takePath。也可以直接用imageUri。
                    //因为Glide直接加载Uri。也可以加载地址。
                    //Glide.with(this)
                    //    .asBitmap()
                    //    .load(imageUri)
                    //    .into(iv);


                    //但是。这里加载的都是拍摄的原图。一般我们都会根据uri。或者path.找到文件。把bitmap取出来。然后做压缩等其他的二次处理。


                    //decodeImage(imageUri);//显示照片

                    //或者直接去裁剪

                    //这里有个坑。就是我们如果想要根据Uri---》图片的真实path.然后拿到File.一般的Uri.
                    // 例如是从图库选择照片并且回来的图片。我们拿到的Uri是这样的:
                    //content://com.android.providers.media.documents/document/image%3A732871
                    //或者这样:content://media/external/images/media/694091
                    //这样我们可以用ImageUtils.getPath()这个里面的一系列方法拿到真实路径。
                    //但是。如果是通过我们的FileProvider拿到的Uri.是这样的:
                    //content://com.example.zongm.testapplication.provider/files_root/files/2018_0713_020952.jpg
                    //这样的路径我们是用ImageUtils.getPath()这个里面的一系列方法是拿不到真实路径的。会报错:
                    //报错信息是:GetDataColumnFail java.lang.IllegalArgumentException: column '_data'does not exist
                    //我们在网上查一下。就可以知道。要想拿到FileProvider得到的Uri的真实图片路径。需要用到反射:
                    //这里大家可以去查一下:这里随便给一个博客地址。:https://blog.csdn.net/u010853225/article/details/80191880
                    // 故这里我们不能用此方法拿到真实路径 String path = ImageUtils.getPath(this, imageUri);
                    cropPhoto(takePath);

                    break;

                case IMAGE_REQUEST_CODE://选择图片成功返回
                    if (data != null && data.getData() != null) {
                        imageUri = data.getData();

                        //直接显示出来
                        //decodeImage(data.getData());
                        //或者去裁剪
                        String path = ImageUtils.getPath(this, imageUri);
                        Log.e("zmm", "选择的图片的虚拟地址是------------>" + data.getData() + "--->" + path);
                        cropPhoto(path);
                    }
                    break;
                case CROP_REREQUEST_CODE:
                    Log.e("zmm", "裁剪以后的地址是------------>" + cropUri);
                    decodeImage(cropUri);
                    break;
            }
        }
    }

    /**
     * 根据uri拿到bitmap
     *
     * @param imageUri 这个Uri是
     */
    private void decodeImage(Uri imageUri) {
        //这样是可以正常拿到bitmap。但是我们知道。这样写。很有可能会oom
        //Bitmap bitmap = ImageUtils.decodeUriAsBitmap(this, imageUri);
        //Log.e("zmm", "初始大小-------------->" + bitmap.getByteCount());//原始大小是47235072
        //iv.setImageBitmap(bitmap);
        //所以我们一般都是把bitmap 进行一次压缩
        try {
            Bitmap bitmapFormUri = ImageUtils.getBitmapFormUri(this, imageUri);
            //Log.e("zmm", "压缩过后------------->" + bitmapFormUri
            //    .getByteCount());//压缩过后2952192
            iv.setImageBitmap(bitmapFormUri);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }


    /**
     * 检查权限
     *
     * @param requestCode
     */
    private void checkPermission(int requestCode) {

        boolean granted = PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(this,
            Manifest.permission_group.CAMERA);
        if (granted) {//有权限
            if (requestCode == REQUEST_CODE_PERMISSION_CAMERA) {
                openCamera();//打开相机
            } else {
                openGallery();//打开图库
            }
            return;
        }
        //没有权限的要去申请权限
        //注意:如果是在Fragment中申请权限,不要使用ActivityCompat.requestPermissions,
        // 直接使用Fragment的requestPermissions方法,否则会回调到Activity的onRequestPermissionsResult
        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA, Manifest
                .permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
            requestCode);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (grantResults.length > 0) {
            boolean flag = true;
            for (int i = 0; i < grantResults.length; i++) {
                if (grantResults[i] != PERMISSION_GRANTED) {
                    flag = false;
                    break;
                }
            }
            //权限通过以后。自动回调拍照
            if (flag) {
                if (requestCode == REQUEST_CODE_PERMISSION_CAMERA) {
                    openCamera();//打开相机
                } else {
                    openGallery();//打开图库
                }
            } else {
                Toast.makeText(this, "请开启权限", Toast.LENGTH_SHORT).show();
            }
        }
    }

    /**
     * 检查SD卡是否存在
     */
    public boolean isSdCardExist() {
        return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
    }

}

嗯。写了很多东西。也很杂乱。总结一下

A:在用相机拍照的时候。报错:

  Caused by: android.os.FileUriExposedException: file:///storage/emulated/0/tencent/MicroMsg/WeiXin/mmexport1530703314161.jpg exposed beyond app through Intent.getData()

说明。你需要用 FileProvider.getUriForFile()来代替原本的Uri.fromFile();

解决办法就是参照网上的方法用FileProvider.


B:如果在过程中报错

GetDataColumnFail java.lang.IllegalArgumentException: column '_data'does not exist

那可能是因为你在Uri转Path的过程中用的Uri是由FileProvider得到的Uri.

解决办法:看看是不是可以换成普通的Uri.如果非得用FileProvider得到的Uri.。你就得用反射的方法来拿到真实路径:具体实现请自行搜索。

C:如果在过程中报错:

Writing exception to parcel
    java.lang.SecurityException: Permission Denial: writing android.support.v4.content.FileProvider uri content://com.example.zongm.testapplication.provider/files_root/files/2018_0713_033250_crop.jpg from pid=10330, uid=10013 requires the provider be exported, or grantUriPermission()

看看是不是在不该使用FileProvider的时候使用了FileproVider。例如裁剪的时候。输出的路径。

至此问题就解决了。

单曲循环《绝口不提!爱你》

每日语录:

在一回首间,才忽然发现,原来,我一生的种种努力,不过只为了周遭的人对我满意而已。为了搏得他人的称许与微笑,我战战兢兢地将自己套入所有的模式所有的桎梏。走到途中才忽然发现,我只剩下一副模糊的面目,和一条不能回头的路。---席慕容《独白》

猜你喜欢

转载自blog.csdn.net/androidzmm/article/details/81025649