大家都知道安卓7.0应用共享文件的行为变更
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file://
URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException
异常。
要在应用间共享文件,您应发送一项 content://
URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用FileProvider类。
首先在manifest里面注册provider。在之前的使用过程中有一点我一直是一知半解。就是配置节点文件
android:resource="@xml/app_file_paths" />
其中app_file_paths这些节点与FileProvider到底有什么关系呢?
我们回到问题的原点FileProvider获取URI,FileProvider通过getUriForFile方法获取。我们点入进去看一下里面的实现
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority, @NonNull File file) {
FileProvider.PathStrategy strategy = getPathStrategy(context, authority);
return strategy.getUriForFile(file);
}
private static FileProvider.PathStrategy getPathStrategy(Context context, String authority) {
HashMap var3 = sCache;
synchronized(sCache) {
FileProvider.PathStrategy strat = (FileProvider.PathStrategy)sCache.get(authority);
if (strat == null) {
try {
strat = parsePathStrategy(context, authority);
} catch (IOException var6) {
throw new IllegalArgumentException("Failed to parse android.support.FILE_PROVIDER_PATHS meta-data", var6);
} catch (XmlPullParserException var7) {
throw new IllegalArgumentException("Failed to parse android.support.FILE_PROVIDER_PATHS meta-data", var7);
}
sCache.put(authority, strat);
}
return strat;
}
}
关键代码parsePathStrategy(context, authority);
private static FileProvider.PathStrategy parsePathStrategy(Context context, String authority) throws IOException, XmlPullParserException {
FileProvider.SimplePathStrategy strat = new FileProvider.SimplePathStrategy(authority);
ProviderInfo info = context.getPackageManager().resolveContentProvider(authority, 128);
XmlResourceParser in = info.loadXmlMetaData(context.getPackageManager(), "android.support.FILE_PROVIDER_PATHS");
if (in == null) {
throw new IllegalArgumentException("Missing android.support.FILE_PROVIDER_PATHS meta-data");
} else {
int type;
while((type = in.next()) != 1) {
if (type == 2) {
String tag = in.getName();
String name = in.getAttributeValue((String)null, "name");
String path = in.getAttributeValue((String)null, "path");
File target = null;
if ("root-path".equals(tag)) {
target = DEVICE_ROOT;
} else if ("files-path".equals(tag)) {
target = context.getFilesDir();
} else if ("cache-path".equals(tag)) {
target = context.getCacheDir();
} else if ("external-path".equals(tag)) {
target = Environment.getExternalStorageDirectory();
} else {
File[] externalMediaDirs;
if ("external-files-path".equals(tag)) {
externalMediaDirs = ContextCompat.getExternalFilesDirs(context, (String)null);
if (externalMediaDirs.length > 0) {
target = externalMediaDirs[0];
}
} else if ("external-cache-path".equals(tag)) {
externalMediaDirs = ContextCompat.getExternalCacheDirs(context);
if (externalMediaDirs.length > 0) {
target = externalMediaDirs[0];
}
} else if (VERSION.SDK_INT >= 21 && "external-media-path".equals(tag)) {
externalMediaDirs = context.getExternalMediaDirs();
if (externalMediaDirs.length > 0) {
target = externalMediaDirs[0];
}
}
}
if (target != null) {
strat.addRoot(name, buildPath(target, path));
}
}
}
return strat;
}
}
可以看到上段代码就是解析xml文件也就是我们的app_file_paths
然后添加到root里面。sCache就是保存了我们注册的provider的一个Map集合,它采用authority作为key。最终返回一个FileProvider.PathStrategy对象这是一个接口我们可以看到里面有两个方法
interface PathStrategy {
Uri getUriForFile(File var1);
File getFileForUri(Uri var1);
}
SimplePathStrategy实现了该接口。我们看看SimplePathStrategy的getUriForFile
@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 mostSpecific = null;
for (Map.Entry root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
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, "/");
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}
path是文件的路径,mRoots是一个保存xml中节点的集合他是在parsePathStrategy()中的调用addRoot添加的
可以看到显示遍历mRoots最终找到一个与path路径最匹配的节点。这里插一下xml文件
external-path是包含external-files-path和external-cache-path的我们直接取external-path这样可以减少mRoot的匹配次数
所以建议少配置节点且把建议external-path的path设置为“.”也就是包括该目录下的全部路径
接下回到上面的代码中。在找到了最匹配的mostSpecific之后采用xml节点中name来代替mostSpecific这一部分。采用content作为schme这其实就是把file://包装成content。
值得一提的是如果配置的path越详细那么隐藏的路径内容也越多,安全性更高。