详解Android7.0及以上版本拍照或者相册选取照片包括裁剪照片时时App崩溃问题

类似问题的在网上已经有大把大把的文章,但是就我碰到的问题而言并没有查看了哪篇文章能够一次性解决我碰到的所有问题,所以在查看了诸多文章我的问题得以解决后,我总结一下我的解决过程,希望对有和我类似问问题的开发者们有帮助。

在手机系统升级到Android7.0后发现在头像上传功能出现了上述问题。而崩溃的原因就是: android .os .FileUriExposedException。
官方文档对该错误的解释,是由于出于安全考虑, Google是反对放宽私有目录的访问权限的,所以收起对私有文件的访问权限是Android将来发展的趋势。
Android7.0中尝试传递 file:// URI 会触发 FileUriExposedException,因为在Android7.0之后Google认为直接使用本地的根目录即file:// URI是不安全的操作,直接访问会抛出FileUriExposedExCeption异常,这就意味着在Android7.0以前我们访问相机拍照存储时,如果使用URI的方式直接存储剪裁图片就会造成这个异常。

下面来说解决方法:
Google为我们提供了FileProvider类,进行一种特殊的内容提供,FileProvider时ContentProvide的子类,它使用了和内容提供器类似的机制来对数据进行保护,可以选择性地将封装过的Uri共享给外部,从而提高了应用的安全性。下面就让我们看一下如何使用这个内容提供者进行数据访问的:
1.使用FileProvider获取Uri就会将以前的file:// URI准换成content:// URI,实现一种安全的应用间数据访问,内容提供者作为Android的四大组件之一,使用同样需要在清单文件AndroidManifest.xml中进行注册的,注册方法如下:
< provider
android :name= "android.support.v4.content.FileProvider"
android :authorities= "com.huanlego.app.provider"//这里的值大家自己填写即可后面会用到
android :exported= "false"
android :grantUriPermissions= "true" >
< meta-data
android :name= "android.support.FILE_PROVIDER_PATHS"
android :resource= "@xml/file_paths" />
</ provider >

2. 在res文件夹下面新建一个xml文件夹,然后在xml文件夹下面建立一个file_paths的xml文件作为provider的共享文件路径
<? xml version= "1.0" encoding= "utf-8" ?>
< resources xmlns: android = " http://schemas.android.com/apk/res/android" ; >
< paths >
< external-path name= "camera_photos" path= "" />
< files-path name= "photos" path= "" />
</ paths >
</ resources >

其中name:一个引用字符串,意思就是可以随便写。  
path:文件夹“相对路径”,完整路径取决于当前的标签类型。  
path可以为空,表示指定目录下的所有文件、文件夹都可以被共享。  
在这个文件中,为每个目录添加一个XML元素指定目录。paths 可以添加多个子路径:< files-path> 分享app内部的存储;< external-path> 分享外部的存储;< cache-path> 分享内部缓存目录。


3.在源代码的相关地方进行判断

拍照:
case R.id . bt_camera :
Intent intentFromCapture = new Intent ( MediaStore . ACTION_IMAGE_CAPTURE );
// 判断存储卡是否可以用,可用进行存储
String state = Environment . getExternalStorageState ();
if (state. equals ( Environment . MEDIA_MOUNTED )) {
File path = Environment . getExternalStoragePublicDirectory ( Environment . DIRECTORY_DCIM );
File file = new File (path, IMAGE_FILE_NAME );

if ( Build.VERSION . SDK_INT >= Build.VERSION_CODES . N ) {
//下面这行代码第二个参数要用到的就是清单文件里刚才我们自己填写的 authorities值
Uri apkUri = FileProvider . getUriForFile ( mContext , "com.huanlego.app.provider" ,file);
intentFromCapture. addFlags ( Intent . FLAG_GRANT_READ_URI_PERMISSION );
intentFromCapture. putExtra ( MediaStore . EXTRA_OUTPUT , apkUri);
} else {
intentFromCapture. putExtra ( MediaStore . EXTRA_OUTPUT , Uri . fromFile (file));
}
}
startActivityForResult (intentFromCapture, CAMERA_REQUEST_CODE );
break ;

相册选取:我这里使用的是第三方的PhotoPicker

case R.id . bt_photo :
PhotoPicker . builder ()
. setPhotoCount ( 1 )
. setShowCamera ( false )
. setShowGif ( true )
. setPreviewEnabled ( false )
. setGridColumnCount ( 3 )
. start ( HeaderCropPictureActivity . this , PhotoPicker . REQUEST_CODE );
break ;

照片选取和拍摄后的回调:
/**
* 拍照选取
*/
case CAMERA_REQUEST_CODE :
// 判断存储卡是否可以用,可用进行存储
String state = Environment . getExternalStorageState ();
if (state. equals ( Environment . MEDIA_MOUNTED )) {
File path = Environment . getExternalStoragePublicDirectory ( Environment . DIRECTORY_DCIM );
File tempFile = new File (path, IMAGE_FILE_NAME );
startPhotoZoom (tempFile);
} else {
ToastUtils . getInstance ( getApplicationContext ()). showToastShort ( " 未找到存储卡,无法存储照片! " );
}
break ;

/**
* 相册选取
*/
case PhotoPicker . REQUEST_CODE :
if ( data != null ) {
ArrayList < String > photos = data . getStringArrayListExtra ( PhotoPicker . KEY_SELECTED_PHOTOS );
String uri = photos. get ( 0 );
if (!uri. contains ( "file://" )) {
uri = "file://" + uri;
}
startPhotoZoom ( new File (uri));
}
break ;

下面是进行的图片裁剪
/**
* 裁剪图片方法实现
*/
public void startPhotoZoom ( File file ) {
Intent intent = new Intent ( "com.android.camera.action.CROP" );
// intent.setDataAndType(uri, "image/*");
// 设置裁剪
intent. putExtra ( "crop" , "true" );
intent. putExtra ( "scale" , true );
intent. putExtra ( "return-data" , false );
intent. putExtra ( MediaStore . EXTRA_OUTPUT , imageUri );
intent. putExtra ( "outputFormat" , Bitmap.CompressFormat . JPEG . toString ());
intent. putExtra ( "noFaceDetection" , true );
intent. putExtra ( "outputX" , 600 );
intent. putExtra ( "outputY" , 600 );

if ( Build.VERSION . SDK_INT >= Build.VERSION_CODES . N ) {
Uri apkUri = MyFileProvider . getUriForFile ( mContext , "com.huanlego.app.provider" , file );
intent. setDataAndType (apkUri, "image/*" );
intent. addFlags ( Intent . FLAG_GRANT_READ_URI_PERMISSION );
} else {
intent. setDataAndType ( Uri . fromFile ( file ), "image/*" );
}

startActivityForResult (intent, RESULT_REQUEST_CODE );
}

网上搜集到的资料大多是这个流程下来的,而我按照这个流程执行的时候又出现了其他的问题

这个问题证明我的清单文件出现了问题,那么ok我们队清单文件的改动只是做了provider的增加,那么问题的根源肯定是在这里应该没错了。
然后继续查询,问题找到了


根据这位博主的文章原因及解决如下
我们项目中可能会用到其他一些第三方sdk有用到拍照功能的话,他也为了适配android7.0也添加了这个节点,此时有些人可能就不知道如何下手了,其实很简单我们只要重写一个类 继承自 FileProvider,然后就按上述方法在添加一个节点就可以了;
很简单,建一个自己的FileProvider就好了。
**
* Created by wzITger on 2017/11/21.
*/

public class MyFileProvider extends FileProvider {
}

下面就是小小的改动:
< provider
android :name= "com.huanlego.app.service.MyFileProvider"
android :authorities= "com.huanlego.app.provider"
android :exported= "false"
android :grantUriPermissions= "true" >
< meta-data
android :name= "android.support.FILE_PROVIDER_PATHS"
android :resource= "@xml/file_paths" />
</ provider >

这样下来似乎就万事大吉了,但是是这样么?不知道别人怎么样,反正我是没有,在拍照截图和从相册选举一些截图文件或者是一些网络图片时一切是正常的
但是接下来,在我从相册中选取拍摄好的照片时就又出现了新的问题:

好吧,很是无语,然后继续找寻完美的那一刻:

FileProvider的坑(Failed to find configured root that contains)

FileProvider所支持的几种path类型

从Android官方文档上可以看出 FileProvider 提供以下几种path类型:
<files-path path="" name="camera_photos" />
该方式提供在应用的内部存储区的文件/子目录的文件。它对应Context.getFilesDir返回的路径:eg:"/data/data/com.jph.simple/files"。
<cache-path name="name" path="path" />
该方式提供在应用的内部存储区的缓存子目录的文件。它对应getCacheDir返回的路径:eg:“/data/data/com.jph.simple/cache”;
<external-path name="name" path="path" />
该方式提供在外部存储区域根目录下的文件。它对应Environment.getExternalStorageDirectory返回的路径:eg:"/storage/emulated/0";
<external-files-path name="name" path="path" />
该方式提供在应用的外部存储区根目录的下的文件。它对应Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)返回的路径。eg:"/storage/emulated/0/Android/data/com.jph.simple/files"。
<external-cache-path name="name" path="path" />
该方式提供在应用的外部缓存区根目录的文件。它对应Context.getExternalCacheDir()返回的路径。eg:"/storage/emulated/0/Android/data/com.jph.simple/cache"。
以上便是Android官方文档上介绍的 FileProvider 所有支持的所以path类型,这些类型在Android手机内部存储区文件共享是可以行的通的,但对于外置SD卡是不行的,如果你想通过 FileProvider.getUriForFile() 获取一个外置SD卡的Uri则会报出如下异常:
这个异常就是和我碰到的问题一样的。那么原因这下我们也应该彻底明了了,我的手机照片时默认存储在SD卡中的嘛,三星手机可以扩展内存的哦。

接下来就是

FileProvider获取对外置SD卡的支持。(里面的一些细节我就不说了,想了解的可以看底下的参考来源)

然后我们就对file_paths.xml进行改动。
将FileProvider的path设置成 root-path ,看能否支持外置SD卡:

<? xml version= "1.0" encoding= "utf-8" ?>
< resources xmlns: android = " http://schemas.android.com/apk/res/android" ; >
< paths >
< external-path path= "" name= "camera_photos" />
< files-path path= "" name= "photos" />
< root-path name= "name" path= "" />
</ paths >
</ resources >

就是高亮的这一行,哦了,接下来我们再来跑一跑我们的程序,ok,我的项目这下是没有任何问题了,小磕小绊都过来了,希望这篇没有太大技术含量只是总结他人结合己身的躺坑文章能帮助大家快速解决这样不是问题的问题,谢谢。


以下是参考来源:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值