Android7.0调用系统相机和裁剪

最近将项目的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>

更多的节点的含义,可以参考下面这个表:
Android7.0调用系统相机和裁剪_第1张图片
经过上面这样的配置,其实就是将某个路径通过别名的形式标记起来了,例如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();
        }
    }
}

你可能感兴趣的:(Android)