android 7.0相机拍照适配

最近,之前一个项目的用户反馈APP拍照崩溃,查看了线上Crash日志,发现是 EVA-AL10ZUK Z2131 两款手机,android 版本都是7.0的,看了下具体的错误消息,如下:

//...
Caused by: android.os.FileUriExposedException: file:///storage/emulated/0/1489548204216.jpg exposed beyond app through ClipData.Item.getUri()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1816)
//...

在 Stackoverflow 查了下,发现是google 在 android N 之后,提高了私有文件的安全性,应用私有目录将被限制访问,无法通过 file:// URI 类型的Uri 进行应用间文件共享,必须使用 content:// URI类型的Ur,并授予 URI 临时访问权限。

下面是官方对7.0权限和文件共享的说明:

具体内容请查看 官方说明-7.0行为变更(无需翻墙)

如下是 android N 相机的适配过程:

一、 在manifest清单文件中注册provider

添加如下代码:

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.puyue.www.moneysteward.provider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths"/>
</provider>

说明:
exported:要求必须为false,为true则会报安全异,
grantUriPermissions:true,表示授予 URI 临时访问权限
authorities:包名.provider,准确的说应该是 applicationId。

二、 指定共享的目录

res 下新建 xml 目录,然后新建 file_paths.xml文件,该文件名无限制,但须和上面 provider 中的 resource 指定的一致,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="temp_photo"
        path="."/>
</paths>

说明:
path=””:它代码根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了,如果你将path设为path=”pictures”
那么它代表着根目录下的pictures目录(/storage/emulated/0/pictures),如果你向其它应用分享pictures目录范围之外的文件是不行的。

三、使用 FileProvider

如下具体的拍照做法:

tempCameraFilePath = Environment
                     .getExternalStorageDirectory() +
                     "/takePic/" +
                     System.currentTimeMillis() + ".jpg";
File file = new File (tempCameraFilePath);
if (!file.exists() )
{
    file.getParentFile().mkdirs();
}
Intent intent = new Intent (MediaStore.ACTION_IMAGE_CAPTURE);
Uri uri = Uri.fromFile (new File (tempCameraFilePath) );

//适配7.0
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M)
{
    uri = FileProvider
          .getUriForFile (this,
                          BuildConfig.APPLICATION_ID +
                          ".provider", file);
}
intent.putExtra (MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult (intent, REQUEST_CODE_CAMERA);
}

需要对手机版本进行判断,否则直接用 FileProvider 在低版本手机上会导致相机 停止运行。注意需要申明下面两个权限:

<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

此外,由于拍照指定了 uri ,在 onActivityResult返回的 data 往往是 空的。

在开发中,除了拍照,经常还会遇见 打开相册选取图片,下面是最近遇到的坑。

对于大多手机,都可以通过 ACTION_PICK 来打开相册,选取图片,如下:

Intent intent = new Intent(Intent.ACTION_PICK, null);
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
startActivityForResult(intent, REQUEST_CODE_ALBUM);

然而对于小米手机,却无法获取返回的路径,以下是对返回的 URI 的处理办法(来自网络):

/**
 * 解决小米手机 相册 返回 null
 *
 * @param intent
 * @return
 */
public Uri getPictureUri (android.content.Intent intent)
{
    Uri uri = intent.getData();
    String type = intent.getType();
    if (uri.getScheme().equals ("file") && (type.contains ("image/") ) )
    {
        String path = uri.getEncodedPath();
        if (path != null)
        {
            path = Uri.decode (path);
            ContentResolver cr = this.getContentResolver();
            StringBuffer buff = new StringBuffer();
            buff.append ("(").append (MediaStore.Images.ImageColumns.DATA).append ("=")
            .append ("'" + path + "'").append (")");
            Cursor cur = cr.query (MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                                   new String[] {MediaStore.Images.ImageColumns._ID},
                                   buff.toString(), null, null);
            int index = 0;
            for (cur.moveToFirst(); !cur.isAfterLast(); cur.moveToNext() )
            {
                index = cur.getColumnIndex (MediaStore.Images.ImageColumns._ID);
                // set _id value
                index = cur.getInt (index);
            }
            if (index == 0)
            {
                // do nothing
            }
            else
            {
                Uri uri_temp = Uri
                               .parse ("content://media/external/images/media/"
                                       + index);
                if (uri_temp != null)
                {
                    uri = uri_temp;
                }
            }
        }
    }
    return uri;
}

然后在 onActivityResult进行处理返回的uri:

@Override
protected void onActivityResult (int requestCode, int resultCode, Intent data)
{
    super.onActivityResult (requestCode, resultCode, data);
    switch (requestCode)
    {
    case REQUEST_CODE_ALBUM:   //相册
    {
        if (resultCode == RESULT_OK)
        {
            if (data != null)
            {
                Uri uri = getPictureUri (data); //处理返回的uri
                String path = "";
                String[] proj = {MediaStore.Images.Media.DATA};
                Cursor cursor = getContentResolver().query (uri, proj, null, null, null);
                if (cursor != null)
                {
                    int index = cursor.getColumnIndexOrThrow (
                                    MediaStore.Images.Media.DATA);
                    cursor.moveToFirst();
                    path = cursor.getString (index);
                    cursor.close();
                    //do something
                }
            }
        }
        break;
    }

    }
}

自己开发 用nexus6 测试毫无问题,却被测试妹子发现了,国内的手机厂商对room的阉割,导致 android 的适配变得很难,最近在做权限的处理时,又一次遇到了各种坑。



   转载规则(或关注公众号:imtianx)


《android 7.0相机拍照适配》imtianx 采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。
 上一篇
Linux 常用命令 Linux 常用命令
之前,服务器 一直都是 window server 系统,主要是桌面的,方便操作,但是同样的配置 ,Centos要比他好很多,就把服务器换成了 Centos 7 ,开始学习使用 linux,并在简短记下。 1 常用命令ls  
2017-03-18
下一篇 
Tomcat中配置单IP多二级域名方法 Tomcat中配置单IP多二级域名方法
由于一级域名 imtianx.cn 作为博客主地址了,为了方便使用,通过二级域名来配置网站。这里简介二级域名的配置方式。作为学生,为了便宜采用 腾讯云 服务器。在 windows server 2012 服务器上配置网站,使用 toncat
  目录