《Android 基础(四十七)》FileProvider
简介
FileProvider,是ContentProvider的子类,通过构建以”content://”开头的Uri取代之前以”file://”开头的Uri,以此实现应用间的文件共享。
由来
官文Android7.0行为变更说明:
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。
要在应用间共享文件,需要改用content:// 格式的URI,并授予 URI 临时访问权限。
实现此类操作最简单的方法就是使用FileProvider。
使用方式
定义FileProvider
FileProvider本身就能根据file生成content:// Uri,所以我们并没有必要去写一个单独的FileProvider子类。但是在某些情况下,我们可以简单的继承FileProvider,修改类名来实现与FileProvider在名字上的区分,毕竟在AndroidManifest.xml中,名字相同的provider是不被允许的。
AndroidManifest.xml中申明FileProvider
<manifest>
...
<application>
...
<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/file_paths" />
</provider>
...
</application>
</manifest>
明确可用文件
上面明确来resource文件为”@xml/file_paths”。那么我们就在file_paths中明确我们的可用位置。
FileProvider只能为你事先指定的目录中的文件生成内容URI。 要指定目录,请使用< paths >元素的子元素指定其存储区域和XML路径。 例如,以下路径元素告诉FileProvider你打算请求私有文件目录下的images /子目录的内容URI
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
...
</paths>
paths元素 | 对应目录 |
---|---|
< root-path/> | “/” |
< files-path name=”name” path=”path” /> | Context.getFilesDir() |
< cache-path name=”name” path=”path” /> | Context.getCacheDir() |
< external-path name=”name” path=”path” /> | Environment.getExternalStorageDirectory() |
< external-files-path name=”name” path=”path” /> | Context#getExternalFilesDir(String) Context.getExternalFilesDir(null) |
< external-cache-path name=”name” path=”path” /> | Context.getExternalCacheDir() |
< external-media-path name=”name” path=”path” /> | Context.getExternalMediaDirs()(API21+) |
这样看可能不太明显,随意新建一个Android工程,打印如上内容,示例工程包名为
cn.onlyloveyd.lazyshare
为File生成Content Uri
- 创建需要用Uri表示的文件File
- 使用getUriForFile()方法获取对应的Uri.
官方示例:
File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);
生成的Uri就是
content://com.mydomain.fileprovider/my_images/default_image.jpg.
临时授权Uri
给通过getUriForFile()方法返回的Uri授权的步骤:
- 调用Context.grantUriPermission(package, Uri, mode_flags)。package为包名,Uri为需要临时授权的content Uri。mode_flags可以为FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION,根据需求而定。通过revokeUriPermission() 或者重启取消授权。
- Intent中setData方法设置Uri
- Intent setFlags方法设置FLAG_GRANT_READ_URI_PERMISSION 或者FLAG_GRANT_WRITE_URI_PERMISSION。
- 发送Intent到另一个App
传递Uri到另一个应用
将content:\ Uri提供给客户端应用程序的方式很多。 一种常见的方法是客户端应用程序通过调用startActivityResult()来启动应用程序,发送一个Intent以启动一个Activity,然后通过setResult() 的方式返回给客户端。
另一种方式是通过调用Intent.setClipData()方法将content:\ Uri放入ClipData对象中,然后将该对象添加到发送给客户端应用程序的Intent中即可。
源码看看
类结构
SimplePathStrategy
static class SimplePathStrategy implements PathStrategy {
private final String mAuthority;
private final HashMap<String, File> mRoots = new HashMap<String, File>();
SimplePathStrategy(String authority) {
mAuthority = authority;
}
/**
* Add a mapping from a name to a filesystem root. The provider only offers
* access to files that live under configured roots.
*/
//读取xml配置文件,建立名称和目录的映射表
void addRoot(String name, File root) {
//名字不能为空
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("Name must not be empty");
}
try {
// Resolve to canonical path to keep path checking fast
root = root.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to resolve canonical path for " + root, e);
}
mRoots.put(name, root);
}
@Override
public Uri getUriForFile(File file) {
String path;
try {
//获取路径
path = file.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
// Find the most-specific root path
Map.Entry<String, File> mostSpecific = null;
//遍历查找文件路径对应的名称
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
//查询未果,说明在xml中未定义
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path);
}
// Start at first char of path under root
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
// Encode the tag and path separately
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
//构建content Uri,这就是最后我们拿到的内容
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}
@Override
public File getFileForUri(Uri uri) {
String path = uri.getEncodedPath();
//通过uri反向寻找,和上面的原理差不多,不赘述
final int splitIndex = path.indexOf('/', 1);
final String tag = Uri.decode(path.substring(1, splitIndex));
path = Uri.decode(path.substring(splitIndex + 1));
final File root = mRoots.get(tag);
if (root == null) {
throw new IllegalArgumentException("Unable to find configured root for " + uri);
}
File file = new File(root, path);
try {
file = file.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
if (!file.getPath().startsWith(root.getPath())) {
throw new SecurityException("Resolved path jumped beyond configured root");
}
return file;
}
}
parsePathStrategy
private static PathStrategy parsePathStrategy(Context context, String authority)
throws IOException, XmlPullParserException {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
//获取Provider信息
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
// 获取"android.support.FILE_PROVIDER_PATHS"对应的xml文件解析对吸纳个;
final XmlResourceParser in = info.loadXmlMetaData(
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
if (in == null) {
throw new IllegalArgumentException(
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
}
int type;
while ((type = in.next()) != END_DOCUMENT) {
if (type == START_TAG) {
final String tag = in.getName();
final String name = in.getAttributeValue(null, ATTR_NAME);
String path = in.getAttributeValue(null, ATTR_PATH);
File target = null;
//"root-path"
if (TAG_ROOT_PATH.equals(tag)) {
target = DEVICE_ROOT;
//"files-path"
} else if (TAG_FILES_PATH.equals(tag)) {
target = context.getFilesDir();
//"cache-path"
} else if (TAG_CACHE_PATH.equals(tag)) {
target = context.getCacheDir();
//"external-path"
} else if (TAG_EXTERNAL.equals(tag)) {
target = Environment.getExternalStorageDirectory();
//"external-files-path"
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
if (externalFilesDirs.length > 0) {
//取数组中的第一个
target = externalFilesDirs[0];
}
// "external-cache-path"
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
}
// "external-media-path" L版本以上才有
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
File[] externalMediaDirs = context.getExternalMediaDirs();
if (externalMediaDirs.length > 0) {
target = externalMediaDirs[0];
}
}
if (target != null) {
strat.addRoot(name, buildPath(target, path));
}
}
}
return strat;
}
从上面这个方法可以很直观的了解到在AndroidManifest.xml文件中定义provider以及对应的共享文件路径定义xml的解析过程。以及xml中tag与真实文件路径的对应关系。
使用场景
拍照
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
.format(new Date()) + ".png";
File file = new File(Environment.getExternalStorageDirectory(), filename);
mCurrentPhotoPath = file.getAbsolutePath();
Uri fileUri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
fileUri = getUriForFile(context,
context.getPackageName() +".fileprovider", file);
} else {
fileUri = Uri.fromFile(file);
}
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
}
……
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_TAKE_PHOTO) {
mIvPhoto.setImageBitmap(BitmapFactory.decodeFile(mCurrentPhotoPath));
}
// else tip?
}
应用安装
// 需要自己修改安装包路径
File file = new File(Environment.getExternalStorageDirectory(),
"/onlyloveyd/base.apk");
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setDataAndType(getUriForFile(context, file), "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
startActivity(intent);
上一篇: 企业网站选择目标关键词的方法
下一篇: 构造函数的继承
推荐阅读