问题现象
我们在开发 app 时避免不了需要添加应用内升级功能。当 app 启动时,如果检测到最新版本,将 apk 安装包从服务器下载下来,执行安装。
安装apk的代码一般写法如下,网上随处可以搜到
public static void installApk(Context context, File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri data = Uri.fromFile(file);
intent.setDataAndType(data, "application/vnd.android.package-archive");
context.startActivity(intent);
}
1
public static void installApk(Context context, File file) {
2
Intent intent = new Intent(Intent.ACTION_VIEW);
3
Uri data = Uri.fromFile(file);
4
intent.setDataAndType(data, "application/vnd.android.package-archive");
5
context.startActivity(intent);
6
}
然而,当我们在Android7.0手机中执行时,会发现报如下错误日志
Caused by: android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/net.csdn.blog.ruancoder/cache/test.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:8933)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8894)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1517)
at android.app.Activity.startActivityForResult(Activity.java:4224)
at android.support.v4.app.BaseFragmentActivityJB.startActivityForResult(BaseFragmentActivityJB.java:50)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:79)
at android.app.Activity.startActivityForResult(Activity.java:4183)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:859)
at android.app.Activity.startActivity(Activity.java:4507)
at android.app.Activity.startActivity(Activity.java:4475)
1
Caused by: android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/net.csdn.blog.ruancoder/cache/test.apk exposed beyond app through Intent.getData()
2
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
3
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
4
at android.content.Intent.prepareToLeaveProcess(Intent.java:8933)
5
at android.content.Intent.prepareToLeaveProcess(Intent.java:8894)
6
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1517)
7
at android.app.Activity.startActivityForResult(Activity.java:4224)
8
at android.support.v4.app.BaseFragmentActivityJB.startActivityForResult(BaseFragmentActivityJB.java:50)
9
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:79)
10
at android.app.Activity.startActivityForResult(Activity.java:4183)
11
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:859)
12
at android.app.Activity.startActivity(Activity.java:4507)
13
at android.app.Activity.startActivity(Activity.java:4475)
原因
我们来看一下FileUriExposedException官方文档
当应用程序将文件以【file://】形式的Uri公开到另一个应用程序时抛出的异常
不鼓励这种曝光方式,因为接收的app可能无法访问你所共享的路径。例如,接收app可能未请求运行时权限Manifest.permission.READ_EXTERNAL_STORAGE ,或者平台可能跨用户配置文件边界[user profile boundaries]共享Uri。
相反,应用程序应使用【content://】形式的Uris,以便平台可以扩展接收应用程序的临时权限以访问资源。
仅针对 Build.VERSION_CODES.N 或更高版本的应用程序抛出此操作。 早期SDK版本的app可以以【file://】形式的Uri共享文件,但强烈建议不要这样做。
x
1
当应用程序将文件以【file://】形式的Uri公开到另一个应用程序时抛出的异常
2
3
不鼓励这种曝光方式,因为接收的app可能无法访问你所共享的路径。例如,接收app可能未请求运行时权限Manifest.permission.READ_EXTERNAL_STORAGE ,或者平台可能跨用户配置文件边界[user profile boundaries]共享Uri。
4
相反,应用程序应使用【content://】形式的Uris,以便平台可以扩展接收应用程序的临时权限以访问资源。
5
6
仅针对 Build.VERSION_CODES.N 或更高版本的应用程序抛出此操作。 早期SDK版本的app可以以【file://】形式的Uri共享文件,但强烈建议不要这样做。
FileProvider官方文档:
解决方案
1、声明FileProvider
首先在清单文件中声明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>
1
<manifest>
2
<application>
3
<provider
4
android:name="android.support.v4.content.FileProvider"
5
android:authorities="${applicationId}.fileprovider"
6
android:exported="false"
7
android:grantUriPermissions="true">
8
<meta-data
9
android:name="android.support.FILE_PROVIDER_PATHS"
10
android:resource="@xml/file_paths" />
11
</provider>
12
</application>
13
</manifest>
其中
- android:name 是固定写法。
- android:authorities 可自定义,是用来标识该 provider 的唯一标识,建议结合包名来保证 authority 的唯一性。
- android:exported 必须设置成 false,否则运行时会报错 java.lang.SecurityException: Provider must not be exported 。
- android:grantUriPermissions 用来控制共享文件的访问权限。
<meta-data>节点中的 android:resource 指定了共享文件的路径,此处的 file_paths 即是该 Provider 对外提供文件的目录的配置文件,存放在 res/xml/ 下。
2、添加 file_paths.xml 文件
文件格式如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<root-path
name="root"
path=""/>
<files-path
name="files"
path=""/>
<cache-path
name="cache"
path=""/>
<external-path
name="external"
path=""/>
<external-files-path
name="external_file_path"
path=""/>
<external-cache-path
name="external_cache_path"
path=""/>
</paths>
</resources>
26
1
<?xml version="1.0" encoding="utf-8"?>
2
<resources>
3
<paths>
4
<root-path
5
name="root"
6
path=""/>
7
<files-path
8
name="files"
9
path=""/>
10
<cache-path
11
name="cache"
12
path=""/>
13
<external-path
14
name="external"
15
path=""/>
16
<external-files-path
17
name="external_file_path"
18
path=""/>
19
<external-cache-path
20
name="external_cache_path"
21
path=""/>
22
</paths>
23
</resources>
其中根元素<paths>是固定的,内部元素可以是以下节点:
- files-path 对应 getFilesDir()
- cache-path 对应getCacheDir()
- external-path 对应Environment.getExternalStorageDirectory()
- external-files-path 对应getExternalFilesDir()
- external-cache-path 对应getExternalCacheDir()
比如,我们将下载的apk文件存放到 sdcard 中的 Android/data/<package>/cache/download 中,file_paths.xml 文件如下:
<external-cache-path name="cache_download" path="download"/>
x
1
<external-cache-path name="cache_download" path="download"/>
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName);
x
1
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName);
在Java代码中使用FileProvider
案例1:安装指定路径路径的apk
public static void installApk(Context context, File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri data;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //判断版本大于等于7.0
data = FileProvider.getUriForFile(context, context.getPackageName( )+ ".fileprovider", file);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //给目标应用一个临时授权
} else {
data = Uri.fromFile(file);
}
intent.setDataAndType(data, "application/vnd.android.package-archive");
context.startActivity(intent);
}
1
public static void installApk(Context context, File file) {
2
Intent intent = new Intent(Intent.ACTION_VIEW);
3
Uri data;
4
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //判断版本大于等于7.0
5
data = FileProvider.getUriForFile(context, context.getPackageName( )+ ".fileprovider", file);
6
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //给目标应用一个临时授权
7
} else {
8
data = Uri.fromFile(file);
9
}
10
intent.setDataAndType(data, "application/vnd.android.package-archive");
11
context.startActivity(intent);
12
}
案例2:获取拍照后指定路径的文件
去拍照并指定保存文件的位置:
private void showCamera(File file) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(mContext, mContext.getPackageName()+".fileprovider", file);
} else {
uri = Uri.fromFile(file);
}
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, REQUEST_CAMERA);
}
x
1
private void showCamera(File file) {
2
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
3
Uri uri;
4
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
5
uri = FileProvider.getUriForFile(mContext, mContext.getPackageName()+".fileprovider", file);
6
} else {
7
uri = Uri.fromFile(file);
8
}
9
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
10
startActivityForResult(intent, REQUEST_CAMERA);
11
}
处理拍照结果:
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == REQUEST_CAMERA && resultCode == Activity.RESULT_OK) {
if (mTmpFile != null) {
//文件还是你 showCamera 时传过去的文件
}
}
}
x
1
2
public void onActivityResult(int requestCode, int resultCode, Intent data) {
3
super.onActivityResult(requestCode, resultCode, data);
4
if(requestCode == REQUEST_CAMERA && resultCode == Activity.RESULT_OK) {
5
if (mTmpFile != null) {
6
//文件还是你 showCamera 时传过去的文件
7
}
8
}
9
}
2018-8-28