文章大纲
- 引言
- 一、FileProvider概述
- 二、FileProvider的使用步骤
- 1、在manifest中声明FileProvider
- 2、定义用于配置给其他应用访问的路径信息的xml文件
- 2.1、< root-path/> 配置设备的根目录
- 2.2、< files-path/> 配置Context.getFilesDir()
- 2.3、< cache-path/> 配置Context.getCacheDir()
- 2.4、< external-files-path/> 配置Context.getExternalFilesDirs()
- 2.5、< external-path/> 配置Environment.getExternalStorageDirectory()
- 2.6、< external-cache-path/> 配置getExternalCacheDir()
- 2.7、< external-media-path/> 配置Context.getExternalMediaDirs()
- 3、创建content://类型的URI
- 4、给Uri授予临时权限
- 5、向另一个App提供内容URI
- PS、一些来自网络相关的工具类
引言
Android 7.0强制启用了所谓的StrictMode的策略,给我们开发者带来最直接的影响就是我们的App无法直接对外暴露file://类型的URI了,若还是通过Uri.fromFile(file)方式来暴露的话就会引发FileUriExposedException。
打开系统相机、录像等时需要使用Intent携带这种类型的URI去实现。
//Android 7.0之前
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mVideo));
//Android 7.0及以上
Uri videoUri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", mVideo);
一、FileProvider概述
FileProvider 继承自ContentProvider,它通过为文件创建content://类型(而非低版本file://类型的URI)的URI来促进与应用程序关联的文件的安全共享。因为对于Android 7.0应用(仅仅对于android 7.0版本的SDK而言,若编译版本低于Api 25则不会受到影响),通过StrictMode Api禁止我们的应用对外部(跨越应用分享)对外暴露file://类型的URI,Android 7.0应用间的文件共享需要使用content://类型的URI分享且需要为其提供临时的文件访问权限(Intent.FLAG_GRANT_READ_URI_PERMISSION和Intent.FLAG_GRANT_WRITE_URI_PERMISSION),而android.support.v4.content.FileProvider正是解决这个问题的官方答案。
二、FileProvider的使用步骤
1、在manifest中声明FileProvider
- name——配置provider的类名,可以配置android.support.v4.content.FileProvider使用默认的v4的FileProvider,也可以配置为自定义的继承FileProvider的provider类(配置全类名)。
- authorities——作为“签名认证”,也可以自定义任意格式的字符串,只需要在代码中获取uri时保持一致,一般传递${applicationId}.fileprovider,在一个App内一般是一个authorities对应一个唯一的ContentProvider。
- grantUriPermissions——使用FileProvider的使用需要我们给暴露的URI赋予临时访问权限(READ和WRITE),设置为true才可以具有临时权限。
- exported——false表示对应的provider不需要对外开放。
- meta-data——配置的是可以访问的文件的路径信息,使用自定义的xml文件进行配置,FileProvider会自动解析xml文件获取配置信息,其中name配置为android.support.FILE_PROVIDER_PATHS,resource为xml对应的路径。
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/export_paths" />
</provider>
ContentProvider是Android中提供的专门用于不同应用间数据交互和共享的组件。ContentProvider实际上是对SQLiteOpenHelper的进一步封装,以一个或多个表的形式将数据呈现给外部应用,通过Uri映射来选择需要操作数据库中的哪个表,并对表中的数据进行增删改查处理。ContentProvider其底层使用了Binder来完成APP进程之间的通信,同时使用匿名共享内存来作为共享数据的载体。ContentProvider支持访问权限管理机制,以控制数据的访问者及访问方式,保证数据访问的安全性。
2、定义用于配置给其他应用访问的路径信息的xml文件
FileProvider只能为预先指定的目录中的文件生成内容URI(A FileProvider can only generate a content URI for files in directories that you specify beforehand),所以需要首先在项目res目录下新建xml目录并创建xml文件(名称任意),并通过< paths>的子节点以XML格式指定其存储区和路径,FileProvider自动解析这个xml文件来为对应的文件生成URI,xml文件里可以定义7个子节点,每个子节点里可以配置两个属性:
-
name——对应URI中的路径部分的值,可以任意值
-
path——配置暴露出去目录的“相对路径”,而完整路径取决于当前的子节点类型,也可以配置.。
注意:path并非IO意义上的路径,而是ContentProvider使用时候根据业务定义的“路径”,所以这个xml文件其实是用于定义FileProvider对应业务的“路径”,下同。
<!--export_paths.xml-->
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="root" path="xiaoi/shrq/" />
<files-path name="files" path="xiaoi/shrq/files/" />
<cache-path name="cache" path="xiaoi/shrq/cache/." />
<!-- path设置为'.'时代表整个SDCard Environment.getExternalStorageDirectory()目录 -->
<external-path name="sdcard" path="."/>
<external-files-path name="external_files2" path="xiaoi/shrq/ext_file/." />
<external-cache-path name="external_cache_path" path="." />
<external-media-path name="ext_media" path="media" />
</paths>
在paths节点下支持以下几个子节点:
2.1、< root-path/> 配置设备的根目录
配置了这个节点就代表根目录/xiaoi/shrq/对应的目录及其子目录被公开暴露出去了,即FileProvider需要给根目录/xiaoi/shrq/对应的目录及其子目录下的文件创建对应的URI,下同。
2.2、< files-path/> 配置Context.getFilesDir()
配置了这个节点就代表context.getFilesDir()/xiaoi/shrq/files/.对应的目录及其子目录被公开暴露出去了。
2.3、< cache-path/> 配置Context.getCacheDir()
配置了这个节点就代表context.getCacheDir()/xiaoi/shrq/cache/.对应的目录及其子目录被公开暴露出去了。
2.4、< external-files-path/> 配置Context.getExternalFilesDirs()
配置了这个节点就代表context.getExternalFilesDirs()/xiaoi/shrq/ext_file/.对应的目录及其子目录被公开暴露出去了。
2.5、< external-path/> 配置Environment.getExternalStorageDirectory()
配置了这个节点就代表Environment.getExternalStorageDirectory()+ /path值/对应的目录及其子目录被公开暴露出去了。
2.6、< external-cache-path/> 配置getExternalCacheDir()
配置了这个节点就代表context.getExternalCacheDir() + /path值/对应的目录及其子目录被公开暴露出去了。
注意external-cache-path在support-v4:24.0.0时并未支持,直到support-v4:25.0.0才支持。
2.7、< external-media-path/> 配置Context.getExternalMediaDirs()
配置了这个节点就代表context.getExternalMediaDirs() + /path值/对应的目录及其子目录被公开暴露出去了。
注意:external-media-path在api 21及以上才生效
3、创建content://类型的URI
URI(Universal Resource Identifier)通用资源标志符代表要操作的数据,Android上每种资源、图像、视频片段等都可以用URI来表示,从广义上来说URI包括URL,URI由三大部分组成scheme、authority 和path(其中authority又分为host和port)固定格式如下scheme://host:port/path,如果按照上面的配置那么对应的URI就如下图所示:
要创建content://类型的Uri需要传入一个已经存在的File对象,如果不存在需要自己new一个对象
private void createFile(String videoName) {
File parentFile = new File(Environment.getExternalStorageDirectory() + CameraHelper.AVATAR_VIDEO);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
mVideo = new File(parentFile.getPath(), videoName+".mp4");
if(mVideo.exists()){
mVideo.delete();
}
try {
mVideo.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
获取Uri
/**
* 传入的authority 就是在清单中配置的值
*/
Uri videoUri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", mVideo);
4、给Uri授予临时权限
- FLAG_GRANT_READ_URI_PERMISSION——表示读取权限;
- FLAG_GRANT_WRITE_URI_PERMISSION——表示写入权限。
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
可以同时或单独使用这两个权限,根据你自己的项目需求而定。
5、向另一个App提供内容URI
使用Intent传递URI,以打开系统自带的录像功能为例:
private void openSysRecording() {
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
在这里的QUALITY参数,值为两个,一个是0,一个是1,代表录制视频的清晰程度,0最不清楚,1最清楚,使用0,录制1分钟大概内存是几兆
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);
//限制时长 ,参数8代表8秒,可以根据需求自己调,最高应该是2个小时,当在这里设置时长之后,录制到达时间,系统会自动保存视频,停止录制
intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 8);
intent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, 1024 * 1024 * 100);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
Uri videoUri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", mVideo);
//在有些版本上若指定MediaStore.EXTRA_OUTPUT的uri值在onActivityResult时data.getData()值就为null,具体看源码的实现
intent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri);
//在这里有录制完成之后的操作,系统会默认把视频放到照片的文件夹中
startActivityForResult(intent, REQ_CODE_RECORD);
}
处理onActivityResult
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(resultCode==RESULT_OK){
if (requestCode == REQ_CODE_RECORD && data != null) {
Uri uri = data.getData();
String path = getRealVideoPath(this, uri);
if (TextUtils.isEmpty(path)) {
return;
}
FileInputStream fis = null;
FileOutputStream fos = null;
String fileName = mVideo.getPath();
try {
fis = new FileInputStream(path.substring(1));
// 创建文件夹
fos = new FileOutputStream(fileName);
byte[] buffer = new byte[1024];
int len = 0;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
从已有的Uri 解析出对应的path:
/**
* 从Uri 解析出对应的path
* @param context
* @param uri
* @return
*/
public static String getRealVideoPath(final Context context, final Uri uri) {
if (null == uri) {
return null;
}
final String scheme = uri.getScheme();
String data = null;
if (scheme == null) {
data = uri.getPath();
} else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
data = uri.getPath();
} else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
Cursor cursor = context.getContentResolver().query(uri, new String[]{MediaStore.Video.VideoColumns.DATA}, null, null, null);
if (null != cursor) {
if (cursor.moveToFirst()) {
int index = cursor.getColumnIndex(MediaStore.Video.VideoColumns.DATA);
if (index > -1) {
data = cursor.getString(index);
}
}
cursor.close();
}
}
return data;
}
PS、一些来自网络相关的工具类
public class FileProvider7 {
public static Uri getUriForFile(Context context, File file) {
Uri fileUri = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
fileUri = getUriForFile24(context, file);
} else {
fileUri = Uri.fromFile(file);
}
return fileUri;
}
private static Uri getUriForFile24(Context context, File file) {
Uri fileUri = android.support.v4.content.FileProvider.getUriForFile(context,
context.getPackageName() + ".fileprovider",
file);
return fileUri;
}
public static void setIntentDataAndType(Context context,
Intent intent,
String type,
File file,
boolean writeAble) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setDataAndType(getUriForFile(context, file), type);
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);
chmod("777", file.getAbsolutePath());//apk放在cache文件中,需要获取读写权限
}
}
public static void chmod(String permission, String path) {
try {
String command = "chmod " + permission + " " + path;
Runtime runtime = Runtime.getRuntime();
runtime.exec(command);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void setIntentData(Context context,
Intent intent,
File file,
boolean writeAble) {
if (Build.VERSION.SDK_INT >= 24) {
intent.setData(getUriForFile(context, file));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (writeAble) {
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
} else {
intent.setData(Uri.fromFile(file));
}
}
public static void grantPermissions(Context context, Intent intent, Uri uri, boolean writeAble) {
int flag = Intent.FLAG_GRANT_READ_URI_PERMISSION;
if (writeAble) {
flag |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
}
intent.addFlags(flag);
List<ResolveInfo> resInfoList = context.getPackageManager()
.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
context.grantUriPermission(packageName, uri, flag);
}
}
}