我们知道Google在Android 7.0以后对文件访问的安全性做了升级,Android 7.0(targetVersion >= 24)以后不允许包含file://xxx类型的intent离开应用,否则会报异常,尤其是在调用系统相机进行拍照/录视频或者是在装apk文件的时候。所以在7.0以后,除了运行时权限申请以外,主要就是这个FileProvider的适配使用了,本文记录一下我在使用过程中遇到的一些问题总结。
首先基本的使用,在AndroidManfiest.xml中配置:
<provider
android:name=".provider.ImageSelectorProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/out_file_paths"/>
provider>
第一个name属性,如果你的应用中只是在app中使用到了一处,这个name可以写成android.support.v4.content.FileProvider就可以了,这里我使用的时候是在一个library module当中,所以是为了防止跟其他module的重名。
具体ImageSelectorProvider实现:
public class ImageSelectorProvider extends FileProvider {
//do nothing 避免与应用中的FileProvider重名
}
就是简单的一个继承FileProvider,什么也没干。
第二个authorities属性,这个是相当于一个授权的字符串,如果你只是使用在一个app当中,这个可以随便写死,但是如果是可以被多个app共用的,那么这个最好写成跟包名一致的前缀,这里我是在一个library module当中使用的,直接引用${applicationId},这样在被其他app依赖使用的时候,就会被替换成具体的应用包名。如果不这样做,当多个应用中包含相同authorities属性值的时候,你会发现无法安装应用,具体请查看INSTALL FAILED CONFLICTING PROVIDER问题完美解决方案
第三个exported属性,false表示我们的provider不需要对外开放。
第三个grantUriPermissions属性,true表示允许获取对文件的临时访问权限。
再往下在meta-data属性中我们要配置一个resource属性,这里resource是设置一个xml文件,放置在res/xml/文件目录下的。
out_file_paths.xml内容:
<resources>
<paths>
<external-path name="camera_out_file" path="TQImageSelector/CameraImage/" />
paths>
resources>
首先这里要注意的是,这个xml文件,如果你只是在你的app当中有多个library module使用FileProvider适配,在不同module的AndroidManfiest.xml中配置的这个xml的文件名必须是不同的,否则会出现问题,如果是相同的名字的话最终这个文件会被合并掉,这个问题也是折腾了好久,所以一定要记得多个模块的话要起一个不同的文件名!
这个xml中可以配置的属性:
:内部存储空间应用私有目录下的 files/ 目录,等同于 Context.getFilesDir()
所获取的目录路径;
:内部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getCacheDir()
所获取的目录路径;
:外部存储空间根目录,等同于 Environment.getExternalStorageDirectory()
所获取的目录路径;
:外部存储空间应用私有目录下的 files/ 目录,等同于Context.getExternalFilesDir(null)
所获取的目录路径;
:外部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir()
;其中每个属性下面可以配置 name 和 path 两个属性,name是相当于一个别名,随便起,不要重复就行,path就是在当前这个属性下面的共享目录,比如这里写的是external-path,path是·TQImageSelector/CameraImage/,那么就表示是在
Environment.getExternalStorageDirectory().getPath()
+TQImageSelector/CameraImage/这个目录下,最终这个目录会被共享访问。如果文件是直接存放在根目录下进行共享,那么path可以写成path="."
这样可以访问根目录下的所有文件。
代码中使用, 以启动相机拍照为例:
public void startCamera() {
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (cameraIntent.resolveActivity(getPackageManager()) != null) {
File cameraFile = FileUtils.createCameraFile(this);
cameraPath = cameraFile.getAbsolutePath();
if (getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.M) {
Uri imageUri = ImageSelectorProvider.getUriForFile(this, getPackageName()+".provider", cameraFile);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(cameraIntent, REQUEST_CAMERA);
} else {
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(cameraFile));
startActivityForResult(cameraIntent, REQUEST_CAMERA);
}
}
}
注意这里创建了一个file,创建的这个file必须是在你前面xml文件的path中配置的目录下面,它会传递到Provider.getUriForFile()
方法中。另外intent最好加上flgIntent.FLAG_GRANT_READ_URI_PERMISSION
, 虽然在清单文件中也配置了。
返回结果的获取
如果你启动相机的时候,创建的保存文件完整路径是记住的,比如用全局变量保存,那么你可以直接在onActivityResult
中访问这个路径,但是假如这个路径没有记住,比如启动相机录制视频我是封装到一个library库当中,文件名的创建是在library内部进行创建的,这样app在使用的时候可能拿不到这个文件名,这时可以在onActivityResult
中去解析这个url来获取路径也是可以的。解析代码:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_CODE_VIDEO_RECORD_SYS_CAMERA_TARGET_N) {
//获取系统Camera录制的视频
Uri uri = data.getData();
if (uri != null) {
//视频文件路径
String fileName = UriUtils.getFileNameByFileProviderUri(this, uri);
if (!TextUtils.isEmpty(fileName)) {
String videoPath = ROOT_DIR + "/" + RecordBySystemCamera.VIDEO_FILE_DIR_TARGET_N + fileName;
File file = new File(videoPath);
//...
}
}
}
}
}
这里用到一个方法getFileNameByFileProviderUri
, 用来从FileProvider提供的uri当中解析文件名,我们把在配置FileProvider时xml中配置的path路径在代码中写成常量,然后用这个常量路径 + 解析的文件名就是完整路径。
getFileNameByFileProviderUri
实现代码:
/**
* 根据FileProvider分享的Uri获取对应的文件名
* @param context
* @param uri
* @return
*/
public static String getFileNameByFileProviderUri(Context context, Uri uri) {
String name = null;
try {
Cursor c = context.getContentResolver().query(uri, null, null, null, null);
if (null != c && c.moveToFirst()) {
int nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
name = c.getString(nameIndex);
c.close();
}
} catch (Exception e) {
e.printStackTrace();
}
return name;
}
参考:
Android7.0适配心得
Android 7.0 行为变更 通过FileProvider在应用间共享文件
Android 7.0 适配中 FileProvider 部分的总结
FileProvider无法获取外置SD卡问题解决方案