美文网首页Android知识Android开发Android开发
Android 7.0适配 -- FileProvider 拍照

Android 7.0适配 -- FileProvider 拍照

作者: Android技术分享 | 来源:发表于2018-02-23 14:58 被阅读0次

    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大组件之一,需要注册)

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="iwangzhe.paizhaocaiqie">
    
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
        <uses-permission android:name="android.permission.CAMERA"/>
    
        <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
            <activity android:name=".MainActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    
            <provider
                android:name="android.support.v4.content.FileProvider"
                android:authorities="${applicationId}.FileProvider"
                android:grantUriPermissions="true"
                android:exported="false">
                <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/file_paths"/>
            </provider>
        </application>
    </manifest>
    

    c. res中新建@xml/file_paths文件(注册FileProvider时用到)

    <?xml version="1.0" encoding="utf-8"?>
    <paths>
        <root-path
            name="root"
            path="" />
    
        <files-path
            name="files"
            path="" />
    
        <cache-path
            name="cache"
            path="" />
    
        <external-path
            name="external"
            path="" />
    
        <external-files-path
            name="external_file"
            path="" />
    
        <external-cache-path
            name="external_cache"
            path="" />
    </paths>
    

    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小米手机

    APP启动后的页面.jpg

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

    拍照、选择相册图片后的 图片裁剪页面 .jpg 显示裁切后的图片.jpg

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

    相关文章

      网友评论

        本文标题:Android 7.0适配 -- FileProvider 拍照

        本文链接:https://www.haomeiwen.com/subject/umuztftx.html