Android 7.0 调取系统相机崩溃解决android.os.FileUriExposedException

写在前面

最近由于廖子尧忙于自己公司的事情和 OkGo (一款专注于让网络请求更简单的网络框架) ,故让LZ 接替维护 ImagePicker(一款支持单、多选、旋转和裁剪的图片选择器),也是处理了诸多bug,最近总算趋于稳定了,这里就把 Android N (API 24) 以上的相机适配方案分享给大家。

Android Nougat 也是被更新很久了,作为一名 Andorid 开发者,我们有义务时刻准备自己调整 TargetSdkVersion 为最近的一个,于是从之前的 23 直接提高到了 25 。

和往常一样,每当我们调整 TargetSdkVersion,我们需要检查我们的代码的每一部分工作的非常好。如果你只是简单地更改代码,我可以说,你的应用程序正在崩溃或故障的高风险。在这种情况下,当你改变你的应用程序的 TargetSdkVersion 24,我们需要检查每一个功能完美的作品在 Android 的牛轧糖(24)以上。
拿到 7.0 的小米 5 测试机后,迫不及待对自己维护的 ImagePicker 测试了一个遍,然而的确和大家所提的 issuse 一样,在调用系统相机的时候直接崩溃了。

到底是什么引发了 7.0 相机崩溃

跟进错误日志到源码发现,在我们调用相机获取 Uri 的时候发生了崩溃。

  

  原因很明显,file:// 不被允许作为一个附加的 Uri 的意图,否则会抛出 FileUriExposedException 。

为什么在 Android Nougat 下 file:// 不被允许?

你可能会很好奇为什么 Android 团队决定改变这种行为。
其实背后有一个很好的理由,如果文件路径被发送到目标应用程序(相机应用程序在这种情况下),文件将完全访问通过相机应用程序的过程,而不仅仅只有发起者能收到。



但让我们考虑一下,实际上是由我们的应用程序去启动摄像头拍照,并保存作为我们的应用程序的代表文件。因此,该文件的访问权限应该是我们的应用程序而不是摄像头应用程序本身。这就是为什么现在 file:// 在 TargetSdkVersion 24 中要求每一位开发者都去完成这个任务。

那到底怎么解决?

既然 file:// 不再被允许,那我们应该怎么处理呢?答案是通过 FileProvider 去解决它。


我们应该怎么让 FileProvider 解决好它。

1、首先是在 AndroidManifest.xml 中申明
<provider
            android:authorities="${applicationId}.provider"
            android:name=".ImagePickerProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>
2、创建一个 provider_paths.xml 文件在 res 文件夹下的 xml 文件夹下。
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external_files" path="."/>
</paths>
3、在适当的地方去替换它
                Uri uri;
                if (VERSION.SDK_INT <= VERSION_CODES.M){
                    uri = Uri.fromFile(takeImageFile);
                }else{
                    /**
                     * 7.0 调用系统相机拍照不再允许使用Uri方式,应该替换为FileProvider
                     * 并且这样可以解决MIUI系统上拍照返回size为0的情况
                     */
                    uri = FileProvider.getUriForFile(activity, ProviderUtil.getFileProviderName(activity), takeImageFile);
                }

                Log.e("nanchen",ProviderUtil.getFileProviderName(activity));
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,uri);

几点说明

对于上面的三步操作,做几点说明:
1、在 AndroidManifest.xml 文件中对 Provider 的 name 属性申明为什么是 .ImagePickerProvider (实际上这是一个继承自 FileProvider 但什么也没实现的类) 而不直接把 name 赋为 android.support.v4.content.FileProvider ?
这是因为 ImagePicker 作为一个图片选择框架,而你的 App 中同样可能会有申明,为了避免 Android Studio 在编译的时候 merge 各个 Module 导致冲突,这里保险起见的申明了一个不一样的名字。

2、为什么在 AndroidManifest.xml 文件中申明的 authority 属性为 ${applicationId}.provider , 而不是固定的名字。

这是因为在 Android 中,要求 authority 必须是唯一的,如果你在定义一个 provider 的时候为它指定一个唯一的 authority,这里且拿 ImagePicker 做比方,假如你在一个 App 上使用了 ImagePicker 作为图片选择框架,而你在另外一个应用中再次使用 ImagePicker 的时候,系统会检查当然已安装应用的 authority 是否和你要安装应用的 authority 相同,如果相同则会弹出下面的警告,并安装失败。


所以我们在定义 authorities 的时候采用 ApplicationId + Provider 的形式,在获取 authorities 的时候,我们可以通过包名 + Provider 的方式获取。代码如下:

package com.lzy.imagepicker.util;

import android.content.Context;

/**
 * 用于解决 Provider 冲突的 util
 *
 * Author: nanchen
 * Email: liushilin520@foxmail.com
 * Date: 2017-03-22  18:55
 */

public class ProviderUtil {

    public static String getFileProviderName(Context context){
        return context.getPackageName()+".provider";
    }
}

写在最后

以上便解决了 Android N 的相机崩溃问题,如有写的不对的地方,欢迎大家在评论区留言。

做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~


nanchen
上一篇:惊现RecyclerView内部Bug???别急,我们慢慢解决它~


下一篇:面试 6:调整数组顺序使奇数位于偶数前面