赞
踩
Android7.0中增加了一些新特性,也对系统安全性进行了提高,具体增加了那些新特性大家可以参考Android的官方文档。
这篇文字我们来说一说对于我们开发者最重要的一项改变。那就是在应用之间共享文件。
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。
我记得第一次关注到FileProvider是又一次很多用户反馈,应用不能自动更新,当安装包下载完成后,一调用安装程序就crash了,当时我很纳闷,查了下后台日志,发现了问题。
当apk下载完成后,我们会调用下面的代码去让用户安装apk:
Uri uri = Uri.fromFile(new File("/sdcard/demo.apk"));
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
一旦调用这个就应用就crash,查看logcat异常如下:
Caused by: android.os.FileUriExposedException: file:///sdcard/demo.apk exposed beyond app through Intent.getData()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8951)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8910)
那这个异常是在哪里报的呢?
是在StrictMode.java中报出来的:
public static void onFileUriExposed(Uri uri, String location) {
final String message = uri + " exposed beyond app through " + location;
if ((sVmPolicyMask & PENALTY_DEATH_ON_FILE_URI_EXPOSURE) != 0) {
throw new FileUriExposedException(message);
} else {
onVmPolicyViolation(null, new Throwable(message));
}
}
这个方法是在Uri中调用的,调用代码如下:
public void checkFileUriExposed(String location) {
if ("file".equals(getScheme()) && !getPath().startsWith("/system/")) {
StrictMode.onFileUriExposed(this, location);
}
}
看见没,这里有一个判断,如果我们uri中的schem是file的话,那么就会报出这个异常。既然我们已经知道了问题所在了,那如何解决这个问题呢?
FileProvider其实是ContentProvider的子类,它的作用也比较明显了,file:///Uri不给用,那么换个Uri为content://来替代。使用步骤如下
首先我们需要在AndroidManifest.xml中注册FileProvider,注册代码如下:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.wms.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path"/>
</provider>
注意上面的注册代码中要一个xml文件,我们下面来看看xml文件应该怎么编写
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<root-path name="name" path=""/>
<files-path name="name" path="" />
<cache-path name="name" path="" />
<external-path name="name" path="" />
<external-files-path name="name" path="" />
<external-cache-path name="name" path="" />
</paths>
</resources>
<root-path />
代表的是系统根目录,类似new File(“/”)<files-path />
代表的是Context.getFilesDir()
<cache-path />
代表的是Context.getCacheDir()
external-path
代表的是Environment.getExternalStorageDirectory()
external-files-path
代表的是 Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)
<external-cache-path
代表的是 Context.getExternalCacheDir()
上面这六个子元素都有相同的属性
1. name属性: 这个属性可以随便指定名称
2. path属性: 代表上面目录的子目录,不能指定为一个当个文件,必须是目录,例如:
<external-path name="my_path" path="test" />
如果你在xml中指定为这段代码,那么代表的是/sdcard/test
目录
要使用content:// 与另外一个应用共享一个文件,你的应用不得不生成content uri,通俗的来讲就是我们需要将uri转化成content://。FileProvider中提供了方法供我们调用。代码如下:
Uri uri = Uri.fromFile(new File("/sdcard/demo.apk"));
Intent intent = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= 24) {
uri = FileProvider.getUriForFile(this, "com.wms.fileprovider", new File("/sdcard/demo.apk"));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Log.e(TAG, "onCreate: uri = " + uri.toString());
intent.setDataAndType(uri, "application/vnd.android.package-archive");
startActivity(intent);
这时候打印的uri如下:
content://com.wms.fileprovider/my_file/demo.apk
FileProvider.getUriForFile()
里面一共传3个参数,第一个是C ontext
, 第二个是我们在AndroidManifest.xml中声明的authorities,第三个参数是我们要转换的File。
有没有注意到上面代码中我们加了一行:
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
如果不加这行代码,我们看下logcat中会跑出异常,但是程序不会crash掉。但是加上这行代码,就没有警告了。
java.lang.SecurityException: Permission Denial: opening provider android.support.v4.content.FileProvider from ProcessRecord{c9a8125 5564:com.miui.packageinstaller/u0a44} (pid=5564, uid=10044) that is not exported from uid 10134
at android.os.Parcel.readException(Parcel.java:1683)
at android.os.Parcel.readException(Parcel.java:1636)
at android.app.ActivityManagerProxy.getContentProvider(ActivityManagerNative.java:4191)
at android.app.ActivityThread.acquireProvider(ActivityThread.java:5528)
鉴于上面出现的问题,我们引出FileProvider的授权问题。
为了给getUriForFile()
授予访问content uri权限,我们应该遵循下面几个步骤:
对content:// Uri
调用 Context.grantUriPermission(package, Uri, mode_flags)
,使用希望的模式标志。这种方式根据mode_flags授予制定的程序临时访问的权限。mode_flags可以设定为 FLAG_GRANT_READ_URI_PERMISSION
或者 FLAG_GRANT_WRITE_URI_PERMISSION
,或者两者都有。这种授权一直会存在,直到你调用 revokeUriPermission()
或者手机重启。
调用 setData将content URI放入Intent 当中
调用 Intent.setFlags() 设置 FLAG_GRANT_READ_URI_PERMISSION
或者 FLAG_GRANT_WRITE_URI_PERMISSION
权限。
发送Intent给其他应用,大多数的时候是调用setResult
上面这种授权的方式很麻烦,建议在使用FileProvider的时候判断下系统版本就ok了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。