使用FileProvider解决Android7.0安装APK出现FileUriExposedException的问题

  • A+
所属分类:Android

绝大多数国产Android App都会内置一个更新功能,也就是把新版本的APK放在服务器上,通过接口获取更新信息并下载,然后进行安装。虽然这种行为被Google严厉禁止,但身处这种环境下还是得妥协的。其他过程咱今天不说,就说说下载完成后的安装。

绝大多数的经验人士都知道以往我们在App内部安装新版本APK的时候,只需要使用非常简单的代码就能实现:

其中,file参数是一个通过APK文件的地址获取的File对象,比如你的APK下载地址是/sdcard/myapp.apk,则传入new File(“sdcard/myapp.apk”)。简单粗暴,效果显著,我表示这几行代码自从几年前写下来之后就很久没动过了,可谓是久经考验。然而到了Android 7.0之后,继续沿用这部分代码的时候,就会发现问题了。

继续调用上述代码,则会得到这样的崩溃,这是怎么回事?首先我们来看一下错误日志,FileUriExposedException是什么东西?字面意思是,文件Uri暴露的异常。官方文档对此有比较详细的介绍:https://developer.android.com/reference/android/os/FileUriExposedException.html

简而言之,当你的应用把file:// Uri暴露给其他App的时候就会出现这种异常,因为接收方Ap可能并不具备访问该共享资源的权限。所以应该用content:// Url来拓展临时权限,这样接收方就能访问到资源了。显然,这是Google为了收紧Android的自由度,提升安全度所做的事情。

我们从Android 7.0的行为变更文档就能找到问题的原因所在了:https://developer.android.com/about/versions/nougat/android-7.0-changes.html?hl=zh-cn

在应用间共享文件

对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。

使用FileProvider解决问题

要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件 https://developer.android.com/training/secure-file-sharing/index.html?hl=zh-cn。

OK,官方已经告诉我们这是怎么回事了,也告诉我们,请使用FileProvider来解决此问题。

  • 什么是FileProvider

https://developer.android.com/reference/android/support/v4/content/FileProvider.html

FileProvider 就是一个ContentProvider的特殊子类,它可以很方便的用来创建content:// Uri 来代替老旧的file:/// Uri从而实现安全便捷的应用间文件共享。因为content:// Uri允许应用间授予临时的特定目录下的文件读写权限,而不是像file:/// Uri那样需要彻底修改文件权限,所以安全性的提高十分显著。

  • 声明FileProvider

由于FileProvider是ContentProvider的子类,所以使用的时候需要在Manifest文件里面进行声明:

android:name="android.support.v4.content.FileProvider":固定写法,不需要做什么变动。当然,如果你自己对FileProvider提供的功能不满意,可以自己继承FileProvider写一个,那就可以改成自己的FileProvider了。

android:authorities="自己的包名.fileprovider":这里是用来进行标记的,按照Android开发的祖传遗训,一般我们都用自己应用的包名,当然别忘了最后的.fileprovider啦。

android:exported="false":尽管这里可true可false,但是填true的话,实际运行中调用会出现java.lang.SecurityException: Provider must not be exported 的问题,所以这里必须设为false。

android:grantUriPermissions="true":这里同样也是固定值,固定为true,这样表示允许授予其他应用以content://Uri形式的临时访问文件的权限。如果设置成false,那就一点意义都没有了。

至于最后的<meta>数据,这又是什么呢?

  • 定义可用文件

这条<meta>数据很关键,其中android:name="android.support.FILE_PROVIDER_PATHS"是固定值,说明这条数据是定义了FileProvider对外提供文件共享的路径。而android:resource="@xml/file_paths"则十分明显的表示,它定义了一个资源文件,而且是在res/xml下的一个名为file_paths的xml文件(你也可以给它改改名字)。那么,这个file_paths.xml文件里面会有什么样的内容呢?

此文件由开发者自行添加,格式如下:

最外层的<paths>标签就不必理会了,我们来看一下内部层次的标签:

<files-path name="name" path="path" />:目录的位置在Context.getFilesDir()的目录,实际上就是/data/data/你的包名/files这个位置。

<cache-path name="name" path="path" />:目录的位置在 getCacheDir(),实际上就是/data/data/你的包名/cache目录。

<external-path name="name" path="path" />:目录的位置在Environment.getExternalStorageDirectory(),实际上就是sdcard的根目录了,一般是/sdcard/,当然实际的路径可能是/storage/emulated/0/这样的。

<external-files-path name="name" path="path" />:目录的位置在Context#getExternalFilesDir(String) Context.getExternalFilesDir(null),实际上就是/sdcard/Android/data/你的包名/files/目录。

<external-cache-path name="name" path="path" />:目录的位置在Context.getExternalCacheDir(),实际上就是/sdcard/Android/data/你的包名/cache/目录。

然后我们再来看一下,name和path这2个属性分别代表了什么:

name:一个URI路径的字段。为了确保安全,name值其实隐藏了你要共享的子目录的真实名称。实际上呢,你要分享的子目录可能是/data/data/你的包名/files/image/,但直接暴露出去是不安全的,通过映射,实现content://Uri的方式把这个目录共享,别的应用在得到你的信息的时候,它是不知道真实路径的。在使用的时候你可以不太关心name值,但实际上它对系统来说非常重要。

path:这个代表了你想要分享的子目录的真实路径,跟name值是形成映射的,这样有了name和path的对应关系,系统才知道你分享出去的content://Uri到底代表了哪个实际的目录,而其他应用则无法得知实际位置。需要注意的是,path定义的是一个子目录,并不是单独的文件。换言之,我们使用FileProvider共享出去的是一个目录,而不是单个文件。以<external-cache-path name="name" path="path" />为例,实际上你的path属性为“path”的时候,这个配置的最终目录是Context.getExternalCacheDir() + path,也就是/sdcard/Android/data/你的包名/cache/path/这个目录,如果你的path的值设置成"mypath",则共享的目录是/sdcard/Android/data/你的包名/cache/mypath/,这下子你应该可以明白path到底代表什么了吧?

  • 搞定Android 7.0的APK安装

前面说了一大通话,为了能够顺利的在Android 7.0以及更高版本的系统下安装APK,现在终于可以实际操作一下了。

首先我们在Manifest文件里面对FileProvider进行声明,这个就不多说了。接下来,在res/xml目录下,建立一个名为file_paths的xml文件,文件内容这么写:

这样代表我们在app里会通过FileProvider把/sdcard/kaelli/这个目录给临时共享出去。

然后呢,把要安装的APK文件放置在/sdcard/kaelli/下面,当然了,实际应用中,我们是用下载的方式从服务器把更高版本的APK下载到这个位置

这里的判断不难理解,如果手机系统版本低于Android 7.0,则还是使用老一套,否则就要走FileProvider路线。其中,apk_file参数就是new File("/sdcard/kaelli/myapp.apk"),FLAG_GRANT_READ_URI_PERMISSION这个FLAG值得注意一下,给intent设置了FLAG_GRANT_READ_URI_PERMISSION后,就代表在启动第三方Activity(实际上就是你要把自己的文件共享给的第三方App)的时候,授予对方临时读取URI所映射的目录的权限。而"Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apk_file);"则通过FileProvider将我们的文件(这里就是新版本的APK文件)的路径转化为了content://Uri,双管齐下就实现了可保证安全的临时文件共享功能,如此一来系统负责处理该intent的应用就能顺利安装APK了。

KaelLi

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: