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 == 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 7.0适配 -- FileProvider 拍照、选择相册、裁切图片, 小米机型适配_第1张图片
APP启动后的页面.jpg

demo中使用了拍照、相册等功能,使用前需要先去动态授权,动态授权的代码请参考:
https://www.jianshu.com/p/8e37e9cf20a5

Android 7.0适配 -- FileProvider 拍照、选择相册、裁切图片, 小米机型适配_第2张图片
拍照、选择相册图片后的 图片裁剪页面 .jpg
Android 7.0适配 -- FileProvider 拍照、选择相册、裁切图片, 小米机型适配_第3张图片
显示裁切后的图片.jpg

Demo下载地址:https://pan.baidu.com/s/1dnaugm

你可能感兴趣的:(Android 7.0适配 -- FileProvider 拍照、选择相册、裁切图片, 小米机型适配)