这里转载下,于连林520wcf 发布的《下载安装APK(兼容Android7.0)》的文章。涉及FileProvider的使用,在此记录参考。java
react
咱们使用手机的时候常常会看到应用程序提示升级,大部分应用内部都须要实现升级提醒和应用程序文件(APK文件)下载。
通常写法都差很少,好比在启动app的时候,经过api接口得到服务器最新的版本号,而后和本地的版本号比较,来判断是否须要弹出提示框下载,固然也能够经过推送的自定义消息来实现。
咱们这里主要讨论的是应用程序下载,并在通知栏提醒下载完成。实现过程大体分为三步:android
建立一个service
在service启动的时候建立一个广播接受者,用于接受下载完成的广播
当BroadcastReceiver接受到下载完成的广播时,开始执行安装。api
主要经过系统提供的DownloadManager进行下载,DownloadManager下载完成会发送广播,具体使用看下面完整的代码。若是详细了解能够参考Android系统下载管理DownloadManager功能介绍及使用示例点击打开连接下面建立新的文件DownloadService.java安全
[java] view plain copy服务器
- public class DownLoadService extends Service {
- /**广播接受者*/
- private BroadcastReceiver receiver;
- /**系统下载管理器*/
- private DownloadManager dm;
- /**系统下载器分配的惟一下载任务id,能够经过这个id查询或者处理下载任务*/
- private long enqueue;
- /**TODO下载地址 须要本身修改,这里随便找了一个*/
- private String downloadUrl="http://dakaapp.troila.com/download/daka.apk?v=3.0";
-
- @Nullable
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
-
- receiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- install(context);
- //销毁当前的Service
- stopSelf();
- }
- };
- registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
- //下载须要写SD卡权限, targetSdkVersion>=23 须要动态申请权限
- RxPermissions.getInstance(this)
- // 申请权限
- .request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- .subscribe(new Action1<Boolean>() {
- @Override
- public void call(Boolean granted) {
- if(granted){
- //请求成功
- startDownload(downloadUrl);
- }else{
- // 请求失败回收当前服务
- stopSelf();
-
- }
- }
- });
- return Service.START_STICKY;
- }
-
- /**
- * 经过隐式意图调用系统安装程序安装APK
- */
- public static void install(Context context) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- // 因为没有在Activity环境下启动Activity,设置下面的标签
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.setDataAndType(Uri.fromFile(
- new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "myApp.apk")),
- "application/vnd.android.package-archive");
- context.startActivity(intent);
- }
-
- @Override
- public void onDestroy() {
- //服务销毁的时候 反注册广播
- unregisterReceiver(receiver);
- super.onDestroy();
- }
-
- private void startDownload(String downUrl) {
- //得到系统下载器
- dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
- //设置下载地址
- DownloadManager.Request request = new DownloadManager.Request(Uri.parse(downUrl));
- //设置下载文件的类型
- request.setMimeType("application/vnd.android.package-archive");
- //设置下载存放的文件夹和文件名字
- request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "myApp.apk");
- //设置下载时或者下载完成时,通知栏是否显示
- request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
- request.setTitle("下载新版本");
- //执行下载,并返回任务惟一id
- enqueue = dm.enqueue(request);
- }
- }
上面代码使用了RxPermissions第三方库动态申请权限,须要在app/build.gradle文件中进行配置app
[java] view plain copyide
- dependencies {
- //...
- compile 'com.tbruyelle.rxpermissions:rxpermissions:0.7.0@aar'
- compile 'io.reactivex:rxjava:1.1.6' //须要引入RxJava
- }
记得要配置服务gradle
[java] view plain copyui
- <application
- ...>
- ...
- <service android:name=".DownLoadService"/>
- </application>
最后在MainActivity中添加按钮,执行操做。
当下载的时候,会有通知栏进度条提示。下载完成会提示安装。不过当前程序若是在Android7.0上就会报错。下面是报错的日志:
[java] view plain copy
- Caused by: android.os.FileUriExposedException:
- file:///storage/emulated/0/Download/myApp.apk exposed beyond app through Intent.getData()
这是因为Android7.0执行了“StrictMode API 政策禁”的缘由,不太小伙伴们不用担忧,能够用FileProvider来解决这一问题,
如今咱们就来一步一步的解决这个问题。
Android 7.0错误缘由
随着Android版本愈来愈高,Android对隐私的保护力度也愈来愈大。
好比:Android6.0引入的动态权限控制(Runtime Permissions),Android7.0又引入“私有目录被限制访问”,“StrictMode API 政策”。
这些更改在为用户带来更加安全的操做系统的同时也为开发者带来了一些新的任务。如何让你的APP可以适应这些改变而不是crash,是摆在每一位Android开发者身上的责任。
“私有目录被限制访问“ 是指在Android7.0中为了提升私有文件的安全性,面向 Android N 或更高版本的应用私有目录将被限制访问。这点相似iOS的沙盒机制。
" StrictMode API 政策" 是指禁止向你的应用外公开 file:// URI。 若是一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常。
上面用到的代码中的Uri.fromFile 其实就是生成一个file://URL。
[java] view plain copy
- //...
- intent.setDataAndType(Uri.fromFile(
- new File(Environment.getExternalStoragePublicDirectory(
- Environment.DIRECTORY_DOWNLOADS),
- "myApp.apk")),
- "application/vnd.android.package-archive");
-
- //....
一旦咱们经过这种办法打开其它程序(这里打开系统包安装器)就认为file:// URI类型的 Intent 离开你的应用。这样程序就会发生异常。
接下来就用FileProvider来解决这一问题。
使用FileProvider
使用FileProvider的大体步骤以下:
第一步:在AndroidManifest.xml清单文件中注册provider,由于provider也是Android四大组件之一,能够简单把它理解为向外提供数据的组件,这种组件在实际开发中用的频率并不高,四大组件均可以在清单文件中进行配置。
[java] view plain copy
- <application
- ...>
- <provider
- android:name="android.support.v4.content.FileProvider"
- android:authorities="com.yll520wcf.test.fileprovider"
- android:grantUriPermissions="true"
- android:exported="false">
- <!--元数据-->
- <meta-data
- android:name="android.support.FILE_PROVIDER_PATHS"
- android:resource="@xml/file_paths" />
- </provider>
- </application>
第二步:指定共享的目录上面配置文件中 android:resource="@xml/file_paths" 指的是当前组件引用 res/xml/file_paths.xml 这个文件。
咱们须要在资源(res)目录下建立一个xml目录,而后建立一个名为“file_paths”(名字能够随便起,只要和在manifest注册的provider所引用的resource保持一致便可)的资源文件,
<files-path/>表明的根目录: Context.getFilesDir()
<external-path/>表明的根目录: Environment.getExternalStorageDirectory()
<cache-path/>表明的根目录: getCacheDir()
上述代码中path="",是有特殊意义的,它代码根目录,也就是说你能够向其它的应用共享根目录及其子目录下任何一个文件了。
若是你将path设为path="pictures",那么它表明着根目录下的pictures目录(eg:/storage/emulated/0/pictures),若是你向其它应用分享pictures目录范围以外的文件是不行的。
第三步:使用FileProvider上述准备工做作完以后,如今咱们就可使用FileProvider了。咱们须要将上述安装APK代码修改成以下
[java] view plain copy
- public static void install(Context context) {
- File file= new File(
- Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
- , "myApp.apk");
- //参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致 参数3 共享的文件
- Uri apkUri =
- FileProvider.getUriForFile(context, "com.com.yll520wcf.test.fileprovider", file);
-
- Intent intent = new Intent(Intent.ACTION_VIEW);
- // 因为没有在Activity环境下启动Activity,设置下面的标签
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- //添加这一句表示对目标应用临时受权该Uri所表明的文件
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
- context.startActivity(intent);
- }
上述代码中主要有两处改变:
将以前Uri改为了有FileProvider建立一个content类型的Uri。
添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);来对目标应用临时受权该Uri所表明的文件。
上述代码经过FileProvider的Uri getUriForFile (Context context, String authority, File file)静态方法来获取Uri该方法中authority参数就是清单文件中注册provider时填写的authority
android:authorities="com.yll520wcf.test.fileprovider"
按照上面步骤修改就能够兼容Android7.0了。
后期修改,以前没有考虑7.0如下的版本
可是若是此程序在Android7.0如下运行又会报错了,咱们须要经过版本判断,当Android7.0及以上须要调用上面的代码,Android7.0如下须要调用7.0如下的代码。这样就OK了。修改install() 方法代码。
[java] view plain copy
- /**
- * 经过隐式意图调用系统安装程序安装APK
- */
- public static void install(Context context) {
- File file = new File(
- Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
- , "myApp.apk");
- Intent intent = new Intent(Intent.ACTION_VIEW);
- // 因为没有在Activity环境下启动Activity,设置下面的标签
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- if(Build.VERSION.SDK_INT>=24) { //判读版本是否在7.0以上
- //参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致 参数3 共享的文件
- Uri apkUri =
- FileProvider.getUriForFile(context, "com.a520wcf.chapter11.fileprovider", file);
- //添加这一句表示对目标应用临时受权该Uri所表明的文件
- 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);
- }