正确使用FIleProvider

  • ~7.28K 字

起因

自动更新时,APP覆盖安装失效了,由于先前测试调试都是手动覆盖安装,因此未发现问题

##错误定位
通过调试发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW dat=file:///storage/emulated/0/CESPIB/ces_pib.apk typ=application/vnd.android.package-archive flg=0x10000000 }
at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:2113)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1739)
at android.app.Activity.startActivityForResult(Activity.java:5343)
at android.support.v4.app.k.startActivityForResult(BaseFragmentActivityJB.java:50)
at android.support.v4.app.p.startActivityForResult(FragmentActivity.java:79)
at android.app.Activity.startActivityForResult(Activity.java:5284)
at android.support.v4.app.p.startActivityForResult(FragmentActivity.java:859)
at android.app.Activity.startActivity(Activity.java:5714)
at android.app.Activity.startActivity(Activity.java:5682)
at com.wuhanins.ces_pib.extra.util.DownloadApkDialogFragment.installApk(DownloadApkDialogFragment.java:166)
at com.wuhanins.ces_pib.extra.util.DownloadApkDialogFragment.access$300(DownloadApkDialogFragment.java:33)
at com.wuhanins.ces_pib.extra.util.DownloadApkDialogFragment$2.handleMessage(DownloadApkDialogFragment.java:150)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:213)
at android.app.ActivityThread.main(ActivityThread.java:8147)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1100)

具体错误出现在将文件转换为Uri跳转到安装apk的intent时出错,想了想,应该是7.0以后限制了APP对于文件uri的使用权限.果然,查看官网后发现了端倪:

在应用间共享文件

对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件。

大意就是说文件的访问权限提高了,不能直接使用”file://“ 的方式来共享文件了,应该使用”content://“ URI的方式来共享文件,并使用FileProvider类来授权。

处理方案

一般情况下我们安装apk会这样操作

1
2
3
4
5
6
private void installApk(String filePath) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(new File(filePath)), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}

但7.0以上要求使用FileProvider来授权访问文件
根据官方的指引:https://developer.android.google.cn/reference/android/support/v4/content/FileProvider ,大概需要以下几个步骤:

  • 定义FileProvider
  • 指定可用文件
  • 生成文件的URI
  • 授予URI临时权限
  • 向另一个应用程序提供内容URI

#####下面来一一介绍一下这几个步骤:

  1. 定义FileProvider
    在AndroidManifest.xml文件中注册provider
1
2
3
4
5
6
7
8
9
10
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.wuhanins.ces_pib.provider"
android:exported="false"
android:grantUriPermissions="true">
<!-- 元数据 -->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/update_apk_paths" />
</provider>

解释一下几个参数的含义:
android:name
文件提供者的类名,固定为”android.support.v4.content.FileProvider”,如果你很牛逼也可以自己写一个类并继承”android.support.v4.content.FileProvider”,然后实现一些扩展的功能。
android:authorities
权限的名字,用于标识provider提供的内容,可以有多个名字,各名字之间用“;”隔开。为了不和其它名字冲突一般使用域名的形式来描述。在实际使用过程中,我们一般会用项目的包名替换,例如

1
android:authorities="${applicationId}.provider"

直接通过引用的方式添加authorities
android:exported
内容提供者是否可供其他应用程序使用,在这里不需要,所以填false
android:grantUriPermissions
是否授权给那些本来无权限访问的人临时访问内容提供者提供的内容,这里填true,不然就没法访问到这个文件了。

  1. 指定可用的文件
    为了指定需要访问的文件,需要在一个xml文件中指定被访问文件的存储路径。
    在res目录下新建一个xml文件夹,然后新建一个文件:update_apk_paths.xml(文件名自己随意起),内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<!--
files-path: 该方式提供在应用的内部存储区的文件/子目录的文件。
它对应Context.getFilesDir返回的路径:eg:”/data/data/com.jph.simple/files”。

cache-path: 该方式提供在应用的内部存储区的缓存子目录的文件。
它对应getCacheDir返回的路径:eg:“/data/data/com.jph.simple/cache”;

external-path: 该方式提供在外部存储区域根目录下的文件。
它对应Environment.getExternalStorageDirectory返回的路径:

external-cache-path: 该方式提供在应用的外部存储区根目录的下的文件。
它对应Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)
返回的路径。eg:”/storage/emulated/0/Android/data/com.jph.simple/files”
-->

<!-- name=“update”
相当于下面的path的别名,为了把真实的路径隐藏起来,这样就只能看到别名,如果按照这个别名路径去找文件的话肯定是找不到的。这个别名自己随便取,我把它叫做“update”

path=""
代表你要分享的真实的子目录名,空字符串代表根目录,注意该值必须是一个子目录,不能是文件名-->
<external-path
name="update"
path="CESPIB" />
</paths>
</resources>

综合来讲,以上配置表明:我要分享一个目录供其它人访问,这个目录就是内部存储区的缓存目录的根目录,即 getCacheDir()的返回值。所有根目录及其子目录下的文件都可以被访问,同时我为这缓存目录取了一个别名叫“update”,以混淆视听。
当你要将自己的目录分享给其他应用使用时就需要下面的操作了,将上面的update_apk_paths.xml文件链接到AndroidManifest.xml中定义的provider中,也就是定义中的“元数据”的内容

1
2
3
4
<!-- 元数据 -->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/update_apk_paths" />

android:name
代表资源的类型,此处为固定值”android.support.FILE_PROVIDER_PATHS”
android:resource
代表资源文件,即update_apk_paths.xml,但是不要后缀名

  1. 生成文件的URI
    用以下方式生成文件的Uri:
1
Uri apkUri = FileProvider.getUriForFile(mContext, "com.wuhanins.ces_pib.provider", apkFile);

其中,第二个参数”com.wuhanins.ces_pib.provider”是在AndroidManifest.xml文件中声明的provider中 android:authorities 元素的值,第三个参数apkFile就是下载下来的保存在缓存目录下的apk文件,假如你是用的是下面的方式声明的authorities

1
android:authorities="${applicationId}.provider"

那么你调用的时候最好是

1
Uri apkUri = FileProvider.getUriForFile(mContext, mContext.getApplicationInfo().packageName+".apk.provider", apkFile);

通过系统API获取当前包名,避免拼写出错
4. 授予URI临时权限
授权有很多种方式:这里只说一种,就是通过Intent addFlags()方法,如:

1
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
  1. 向另一个应用程序提供内容URI
    用startActivity(intent)启动一个应用就可以了,被启动的应用就有权限访问你提供的文件了,但要注意必须添加这句:
1
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

最后

安装apk的方法就改造成了下面这样了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void installApk(String filePath) {
File apkFile = new File(filePath);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 7.0 以上
Uri apkUri = FileProvider.getUriForFile(mContext, "com.wuhanins.ces_pib.provider", apkFile);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
// 7.0以下
Uri uri = Uri.fromFile(apkFile);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
}
mContext.startActivity(intent);
}

自动更新失败的问题完美解决,其他例如调用系统相机,调用系统相册等等都可以类推了,但是切记一定要添加上Flag

1
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
赞助喵
非常感谢您的喜欢!
赞助喵
分享这一刻
让朋友们也来瞅瞅!