下載安裝APK(兼容Android7.0)

我們使用手機的時候經常會看到應用程序提示升級,大部分應用內部都需要實現升級提醒和應用程序文件(APK文件)下載。

一般寫法都差不多,比如在啓動app的時候,通過api接口獲得服務器最新的版本號,然後和本地的版本號比較,來判斷是否需要彈出提示框下載,當然也可以通過推送的自定義消息來實現。

我們這裏主要討論的是應用程序下載,並在通知欄提醒下載完成。
實現過程大致分爲三步:

  1. 創建一個service
  2. 在service啓動的時候創建一個廣播接受者,用於接受下載完成的廣播
  3. 當BroadcastReceiver接受到下載完成的廣播時,開始執行安裝。

主要通過系統提供的DownloadManager進行下載,DownloadManager下載完成會發送廣播,具體使用看下面完整的代碼。如果詳細瞭解可以參考Android系統下載管理DownloadManager功能介紹及使用示例下面創建新的文件DownloadService.java

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文件中進行配置

dependencies {
    //...
    compile 'com.tbruyelle.rxpermissions:rxpermissions:0.7.0@aar'
    compile 'io.reactivex:rxjava:1.1.6' //需要引入RxJava
}

記得要配置服務

<application
  ...>
    ...
    <service android:name=".DownLoadService"/>
</application>

最後在MainActivity中添加按鈕,執行操作。運行結果:

當下載的時候,會有通知欄進度條提示。下載完成會提示安裝。不過當前程序如果在Android7.0上就會報錯。下面是報錯的日誌:

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。

//...
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四大組件之一,可以簡單把它理解爲向外提供數據的組件,這種組件在實際開發中用的頻率並不高,四大組件都可以在清單文件中進行配置。

<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>

注意:

  • exported:要求必須爲false,爲true則會報安全異常。
  • grantUriPermissions:true,表示授予 URI 臨時訪問權
    限。
  • authorities 組件標識,按照江湖規矩,都以包名開頭,避免和其它應用發生衝突。

第二步:指定共享的目錄
上面配置文件中 android:resource="@xml/file_paths" 指的是當前組件引用 res/xml/file_paths.xml 這個文件。

我們需要在資源(res)目錄下創建一個xml目錄,然後創建一個名爲“file_paths”(名字可以隨便起,只要和在manifest註冊的provider所引用的resource保持一致即可)的資源文件,內容如下:

  • 代表的根目錄: Context.getFilesDir()
  • 代表的根目錄: Environment.getExternalStorageDirectory()
  • 代表的根目錄: getCacheDir()

上述代碼中path=”“,是有特殊意義的,它代碼根目錄,也就是說你可以向其它的應用共享根目錄及其子目錄下任何一個文件了。

如果你將path設爲path="pictures",那麼它代表着根目錄下的pictures目錄(eg:/storage/emulated/0/pictures),如果你向其它應用分享pictures目錄範圍之外的文件是不行的。

第三步:使用FileProvider
上述準備工作做完之後,現在我們就可以使用FileProvider了。
我們需要將上述安裝APK代碼修改爲如下

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);
}

上述代碼中主要有兩處改變:
1. 將之前Uri改成了有FileProvider創建一個content類型的Uri。
2. 添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);來對目標應用臨時授權該Uri所代表的文件。

上述代碼通過FileProviderUri 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() 方法代碼。

    /**
     * 通過隱式意圖調用系統安裝程序安裝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);
    }

參考文獻

android應用開發app手動更新通知欄下載實踐
Android7.0適配教程,心得

更多精彩請關注微信公衆賬號likeDev
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章