這裏轉載下,於連林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);
- }