转载请注明出处:http://www.jianshu.com/p/b566fa29a76e
本文出自Shawpoo的简书
我的博客:CSDN博客
前言
昨天在开发的时候遇到这样一个问题,在APP中更新版本下载完最新的apk之后没有跳转到应用安装页面。然后我换了个手机又进行测试了一下是可以的,这就怪了。我的代码是这样写的:
/**
* @param file
* @return
* @Description 安装apk
*/
public void installApk(File file) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file),
"application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
打开安装页面不就是这样吗?难道还有其他的方法?正在赶项目的我遇到这个问题真是一脸懵逼,不知所措...
查看了Studio的loacat才发现抛了一个异常:
后来发现只有Android7.0的系统会报这个错:
android.os.FileUriExposedException
为什么会这样?难道是系统版本搞的鬼?没错,大胸弟,你的猜想一点没错,导致这个错误就是由于Android7.0系统引起的。
查询Android开发官网可知:
了解:Android7.0 系统权限更改
为了提高私有文件的安全性,面向 Android 7.0 或更高版本的应用私有目录被限制访问 (0700)。此设置可防止私有文件的元数据泄漏,如它们的大小或存在性。此权限更改有多重副作用:
私有文件的文件权限不应再由所有者放宽,为使用 MODE_WORLD_READABLE
和/或 MODE_WORLD_WRITEABLE而进行的此类尝试将触发 SecurityException。传递软件包网域外的 file:// URI 可能给接收器留下无法访问的路径。因此,尝试传递 file://URI 会触发 FileUriExposedException。分享私有文件内容的推荐方法是使用 FileProvider。
DownloadManager不再按文件名分享私人存储的文件。旧版应用在访问 COLUMN_LOCAL_FILENAME时可能出现无法访问的路径。面向 Android 7.0 或更高版本的应用在尝试访问 COLUMN_LOCAL_FILENAME时会触发 SecurityException。通过使用 [DownloadManager.Request.setDestinationInExternalFilesDir()](https://developer.android.google.cn/reference/android/app/DownloadManager.Request.html#setDestinationInExternalFilesDir(android.content.Context, java.lang.String, java.lang.String)或 [DownloadManager.Request.setDestinationInExternalPublicDir()](https://developer.android.google.cn/reference/android/app/DownloadManager.Request.html#setDestinationInExternalPublicDir(java.lang.String, java.lang.String))将下载位置设置为公共位置的旧版应用仍可以访问 COLUMN_LOCAL_FILENAME中的路径,但是我们强烈反对使用这种方法。对于由 DownloadManager公开的文件,首选的访问方式是使用[ContentResolver.openFileDescriptor()](https://developer.android.google.cn/reference/android/content/ContentResolver.html#openFileDescriptor(android.net.Uri, java.lang.String))。
上面三条更改总之就是,对用户私有的文件访问更加严格,在访问的时候需要使用特定的类和方法,而不像之前的系统那么容易,虽然这种限制不能完全得到控制,但是官方强烈反对放宽私有目录的权限。
看到上面第二条,貌似和安装APK所报的异常一样,根据提示,看来需要使用FileProvider才能解决此异常的出现了。
进入正题:开始解决异常
1、定义FileProvider
在Androidmanifest.xml文件中声明:
<manifest>
...
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.shawpoo.app.fileprovider" //自定义名字 为避免重复建议设为:包名.fileprovider
android:exported="false"
android:grantUriPermissions="true">
...
</provider>
...
</application>
</manifest>
2、指定可用的文件路径
在项目的res目录下,创建xml文件夹,并新建一个file_paths.xml文件。通过这个文件来指定文件路径:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/" />
...
</paths>
有多种指定路径,在<paths>标签内应至少包含一种,或者多种。
- a、表示应用程序内部存储区中的文件/子目录中的文件
<files-path name="name" path="image" />
等同于Context.getFileDir() : /data/data/com.xxx.app/files/image
- b、表示应用程序内部存储区缓存子目录中的文件
<cache-path name="name" path="image" />
等同于Context.getCacheDir() : /data/data/com.xxx.app/cache/image
- c、表示外部存储区根目录中的文件
<external-path name="name" path="image" />
等同于Environment.getExternalStorageDirectory() : /storage/emulated/image
- d、表示应用程序外部存储区根目录中的文件
<external-files-path name="name" path="image" />
等同于Context.getExternalFilesDir(String) / Context.getExternalFilesDir(null) : /storage/emulated/0/Android/data/com.xxx.app/files/image
- e、表示应用程序外部缓存区根目录中的文件
<external-cache-path name="name" path="image" />
等同于Context.getExternalCacheDir() : /storage/emulated/0/Android/data/com.xxx.app/cache/image
3、引用指定的路径
在刚才Androidmanifest.xml中声明的provider进行关联:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.shawpoo.app.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
4、生成文件的Uri
File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "com.shawpoo.app.fileprovider", newFile);
5、给Uri授予临时权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
所以最终安装apk的方法可以这么写了:
/**
* @param file
* @return
* @Description 安装apk
*/
protected void installApk(File file) {
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(context, "com.shawpoo.app.fileprovider", file); //与manifest中定义的provider中的authorities="com.shawpoo.app.fileprovider"保持一致
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
context.startActivity(intent);
}
总结:好多东西还是得多查API呐~虽然英文是硬伤...