1. 程式人生 > >安卓開發實戰之app之版本更新升級(DownloadManager和http下載)完整實現

安卓開發實戰之app之版本更新升級(DownloadManager和http下載)完整實現

前言

本文將講解app的升級與更新。一般而言使用者使用App的時候升級提醒有兩種方式獲得:

  • 一種是通過應用市場 獲取
  • 一種是開啟應用之後提醒使用者更新升級

而更新操作一般是在使用者點選了升級按鈕之後開始執行的,這裡的升級操作也分為兩種形式:

  • 一般升級
  • 強制升級

app升級操作:

  • 應用市場的app升級

在App Store中升級需要為App Store上傳新版App,我們在新版本完成之後都會上傳到App Store中,在稽核完成之後就相當於完成了這個應用市場的釋出了,也就是釋出上線了。這時候如果使用者安裝了這個應用市場,那麼就能看到我們的App有新版本的升級提醒了。

  • 應用內升級

除了可以在應用市場升級,我們還可以在應用內升級,在應用內升級主要是通過呼叫伺服器端介面獲取應用的升級資訊,然後通過獲取的伺服器升級應用資訊與本地的App版本比對,若伺服器下發的最新的App版本高於本地的版本號,則說明有新版本釋出,那麼我們就可以執行更新操作了,否則忽略掉即可。

顯然應用市場提醒的升級不是我們的重點,本篇主要是對於app升級的場景來進行不同角度的實現,便於以後開發過程中直接拿去用就ok了。

伺服器端:

  • 服務端提供一個介面,或者網址,這裡提供一個網址如下:
http://192.168.191.1:8081/update

一般作為一個安卓程式設計師要測試還得寫一個服務端(醉了),這裡我就使用nodejs來搞一個本地的伺服器來測試下app的版本更新檢驗。

  • 根據請求的結果,我這裡就寫一個簡單的json
{"data":{
  "appname": "hoolay.apk",
  "serverVersion": "1.0.2",
  "serverFlag": "1",
  "lastForce" : "1",
  "updateurl": "http://releases.b0.upaiyun.com/hoolay.apk",
  "upgradeinfo": "V1.0.2版本更新,你想不想要試一下哈!!!"
},
  "error_code":"200","error_msg" :"蛋疼的認識"}

然後我電腦上是裝了webstrom的,沒有裝也沒有關係但是必須有nodejs,現在都自帶了express,表示並沒有學過,所以簡單的寫個express_demo.js:

var express = require('express');
var app = express();
var fs = require("fs");
//此處設定為get請求,app裡面直接寫 (本機ip:8081/update)
app.get('/update', function (req, res) {//http://127.0.0.1:8081/update
    fs.readFile( __dirname + "/" + "version.json", 'utf8', function (err, data) {//讀取相同目錄下的version.json檔案
        console.log( data );//列印json資料
        res.end( data );//把json資料response回去
    });
})
var server = app.listen(8081, function () {//埠我這裡寫的是8081
    var host = server.address().address
    var port = server.address().port
    console.log("應用例項,訪問地址為 http://%s:%s", host, port)

})

有webstrom的直接選中檔案run就ok了,沒有直接 node express_demo.js,可以直接瀏覽器開啟:http://127.0.0.1:8081/update

  • 效果如下:

這裡寫圖片描述

上圖為開啟瀏覽器後的顯示結果。

這裡寫圖片描述

上圖為webstrom的終端顯示結果。

客戶端需要實現:

我們知道不同的需求有不同的操作方法和介面顯示:

  1. 從是否為app內部下載還是通知欄更新:

    • app內下載更新

    這時我們必須等下載安裝完全後才能進行操作,效果是這樣的:

    這裡寫圖片描述

    • 通知欄下載更新

    這種情況是不在應用內更新,放在通知欄並不會影響當前app的使用,效果是這樣的:

  2. app更新分3種:強制更新,推薦更新,無需更新

    • 強制更新

      這裡寫圖片描述

    • 推薦更新

      這裡寫圖片描述

    • 無需更新

      這裡寫圖片描述

具體思路:

  1. 實現bean用於對接後端介面實現app的更新(不寫網路請求模擬本地資料也需要這個模型)
  2. 使用retrofit來請求版本更新介面
  3. 下載apk我們分別使用DownloadManager和普通的httpurlconnection
  4. 通過BroadcastReceiver來監聽是否下載完成

準備bean

首先我們要去解析服務端給的json,那麼我們就要來建立一個bean類了,這裡是嚴格根據json檔案的格式來的:

package com.losileeya.appupdate.bean;
/**
 * User: Losileeya ([email protected])
 * Date: 2016-09-27
 * Time: 11:20
 * 類描述:版本更新的實體與你伺服器的欄位相匹配
 * @version :
 */
public class UpdateAppInfo  {
    public UpdateInfo data; // 資訊
    public Integer error_code; // 錯誤程式碼
    public String error_msg; // 錯誤資訊
    public static class UpdateInfo{
        // app名字
        public String appname;
        //伺服器版本
        public String serverVersion;
        //伺服器標誌
        public String serverFlag;
        //強制升級
        public String lastForce;
        //app最新版本地址
        public String updateurl;
        //升級資訊
        public String upgradeinfo;
        get...
        set...
    }
        get...
        set...
}

網路介面的實現

這裡使用retrofit和rxjava來練筆

先加入 依賴

  compile 'io.reactivex:rxandroid:1.1.0' // RxAndroid
  compile 'io.reactivex:rxjava:1.1.0' // 推薦同時載入RxJava

  compile 'com.squareup.retrofit:retrofit:2.0.0-beta2' // Retrofit網路處理
  compile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta2' // Retrofit的rx解析庫
  compile 'com.squareup.retrofit:converter-gson:2.0.0-beta2' // Retrofit的gson庫

接下來網路介面的定製:

public interface ApiService {
    //實際開發過程可能的介面方式
     @GET("update")
    Observable<UpdateAppInfo> getUpdateInfo(@Query("appname") String   appname, @Query("serverVersion") String appVersion);
    //以下方便版本更新介面測試
    @GET("update")
    Observable<UpdateAppInfo> getUpdateInfo();
}

通過工廠模式來建立ApiService :

public class ServiceFactory {
    private static final String BASEURL="http://192.168.191.1:8081/";
    public static <T> T createServiceFrom(final Class<T> serviceClass) {
        Retrofit adapter = new Retrofit.Builder()
                .baseUrl(BASEURL)
             .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 新增Rx介面卡
                .addConverterFactory(GsonConverterFactory.create()) // 新增Gson轉換器
                .build();
        return adapter.create(serviceClass);
    }
}

版本檢測介面的使用:

 /**
     * 檢查更新
     */
    @SuppressWarnings("unused")
    public static void checkUpdate(String appCode, String curVersion,final CheckCallBack updateCallback) {
     ApiService apiService=   ServiceFactory.createServiceFrom(ApiService.class);
        apiService.getUpdateInfo()//測試使用
                //   .apiService.getUpdateInfo(appCode, curVersion)//開發過程中可能使用的
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<UpdateAppInfo>() {
                    @Override
                    public void onCompleted() {
                    }
                    @Override
                    public void onError(Throwable e) {
                    }
                    @Override
                    public void onNext(UpdateAppInfo updateAppInfo) {
                        if (updateAppInfo.error_code == 0 || updateAppInfo.data == null ||
                                updateAppInfo.data.updateurl == null) {
                            updateCallback.onError(); // 失敗
                        } else {
                            updateCallback.onSuccess(updateAppInfo);
                        }
                    }
                });
    }

附上結果回撥監聽:

  public interface CheckCallBack{//檢測成功或者失敗的相關介面
        void onSuccess(UpdateAppInfo updateInfo);
        void onError();
    }

具體使用介面的處理:

 //網路檢查版本是否需要更新
        CheckUpdateUtils.checkUpdate("apk", "1.0.0", new CheckUpdateUtils.CheckCallBack() {
            @Override
            public void onSuccess(UpdateAppInfo updateInfo) {
               String isForce=updateInfo.data.getLastForce();//是否需要強制更新
               String downUrl= updateInfo.data.getUpdateurl();//apk下載地址
               String updateinfo = updateInfo.data.getUpgradeinfo();//apk更新詳情
               String appName = updateInfo.data.getAppname();
                if(isForce.equals("1")&& !TextUtils.isEmpty(updateinfo)){//強制更新
                    forceUpdate(MainActivity.this,appName,downUrl,updateinfo);
                }else{//非強制更新
                    //正常升級
    normalUpdate(MainActivity.this,appName,downUrl,updateinfo);
                }
            }
            @Override
            public void onError() {
                noneUpdate(MainActivity.this);
            }
        });

實在不想寫網路也好,直接使用假想資料做相關操作如下:

  UpdateAppInfo.UpdateInfo  info =new UpdateAppInfo.UpdateInfo();
             info.setLastForce("1");
        info.setAppname("我日你");
        info.setUpgradeinfo("whejjefjhrherkjreghgrjrgjjhrh");
       info.setUpdateurl("http://releases.b0.upaiyun.com/hoolay.apk");
        if(info.getLastForce().equals("1")){//強制更新      forceUpdate(MainActivity.this,info.getAppname(),info.getUpdateurl(),info.getUpgradeinfo());
                }else{//非強制更新
                    //正常升級   normalUpdate(MainActivity.this,info.getAppname(),info.getUpdateurl(),info.getUpgradeinfo());
                }

更新dialog的使用注意:

 private void forceUpdate(final Context context, final String appName, final String downUrl, final String updateinfo) {
        mDialog = new AlertDialog.Builder(context);
        mDialog.setTitle(appName+"又更新咯!");
        mDialog.setMessage(updateinfo);
        mDialog.setPositiveButton("立即更新", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                if (!canDownloadState()) {
                    showDownloadSetting();
                    return;
                }
                //      DownLoadApk.download(MainActivity.this,downUrl,updateinfo,appName);
     AppInnerDownLoder.downLoadApk(MainActivity.this,downUrl,appName);
            }
        }).setCancelable(false).create().show();
    }

上面以強制更新舉個例子,因為AlertDialog在不同的版本下面表現的美觀度不一致,所以我們需要

import android.support.v7.app.AlertDialog;

然後顯然是不能按返回鍵取消的,我們需要

.setCancelable(false)

使用谷歌推薦的DownloadManager實現下載

Android自帶的DownloadManager模組來下載,在api level 9之後,我們通過通知欄知道, 該模組屬於系統自帶, 它已經幫我們處理了下載失敗、重新下載等功能。整個下載 過程全部交給系統負責,不需要我們過多的處理。

DownLoadManager.Query:主要用於查詢下載資訊。

DownLoadManager.Request:主要用於發起一個下載請求。

先看下簡單的實現:

建立Request物件的程式碼如下:

DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkurl));
   //設定在什麼網路情況下進行下載
   request.setAllowedNetworkTypes(Request.NETWORK_WIFI);
   //設定通知欄標題
   request.setNotificationVisibility(Request.VISIBILITY_VISIBLE);
   request.setTitle("下載");
   request.setDescription("apk正在下載");
   request.setAllowedOverRoaming(false);
   //設定檔案存放目錄
   request.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, "mydown");

取得系統服務後,呼叫downloadmanager物件的enqueue方法進行下載,此方法返回一個編號用於標示此下載任務:

downManager = (DownloadManager)getSystemService(Context.DOWNLOAD_SERVICE);
id= downManager.enqueue(request);

這裡我們可以看下request的一些屬性:

addRequestHeader(String header,String value):新增網路下載請求的http頭資訊
allowScanningByMediaScanner():用於設定是否允許本MediaScanner掃描。
setAllowedNetworkTypes(int flags):設定用於下載時的網路型別,預設任何網路都可以下載,提供的網路常量有:NETWORK_BLUETOOTH、NETWORK_MOBILE、NETWORK_WIFI。
setAllowedOverRoaming(Boolean allowed):用於設定漫遊狀態下是否可以下載
setNotificationVisibility(int visibility):用於設定下載時時候在狀態列顯示通知資訊
setTitle(CharSequence):設定Notification的title資訊
setDescription(CharSequence):設定Notification的message資訊
setDestinationInExternalFilesDir、setDestinationInExternalPublicDir、 setDestinationUri等方法用於設定下載檔案的存放路徑,注意如果將下載檔案存放在預設路徑,那麼在空間不足的情況下系統會將檔案刪除,所 以使用上述方法設定檔案存放目錄是十分必要的。

具體實現思路:

  1. 我們通過downloaderManager來下載apk,並且本地儲存downManager.enqueue(request)返回的id值,並且通過這個id獲取apk的下載檔案路徑和下載的狀態,並且通過狀態來更新通知欄的顯示。

  2. 第一次下載成功,彈出安裝介面

    如果使用者沒有點選安裝,而是按了返回鍵,在某個時候,又再次使用了我們的APP

    如果下載成功,則判斷本地的apk的包名是否和當前程式是相同的,並且本地apk的版本號大於當前程式的版本,如果都滿足則直接啟動安裝程式。

具體程式碼實現:

檔案下載管理的實現,包括建立request和加入佇列下載,通過返回的id來獲取下載路徑和下載狀態。

public class FileDownloadManager {
    private DownloadManager downloadManager;
    private Context context;
    private static FileDownloadManager instance;

    private FileDownloadManager(Context context) {
        downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
        this.context = context.getApplicationContext();
    }

    public static FileDownloadManager getInstance(Context context) {
        if (instance == null) {
            instance = new FileDownloadManager(context);
        }
        return instance;
    }
    /**
     * @param uri
     * @param title
     * @param description
     * @return download id
     */
    public long startDownload(String uri, String title, String description,String appName) {
        DownloadManager.Request req = new DownloadManager.Request(Uri.parse(uri));
        req.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
        //req.setAllowedOverRoaming(false);
 req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        //設定檔案的儲存的位置[三種方式]
        //第一種
        //file:///storage/emulated/0/Android/data/your-package/files/Download/update.apk
        req.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, appName+".apk");
        //第二種
        //file:///storage/emulated/0/Download/update.apk
        //req.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "update.apk");
        //第三種 自定義檔案路徑
        //req.setDestinationUri()


        // 設定一些基本顯示資訊
        req.setTitle(title);
        req.setDescription(description);
        //req.setMimeType("application/vnd.android.package-archive");
        return downloadManager.enqueue(req);//非同步
        //dm.openDownloadedFile()
    }
    /**
     * 獲取檔案儲存的路徑
     *
     * @param downloadId an ID for the download, unique across the system.
     *                   This ID is used to make future calls related to this download.
     * @return file path
     * @see FileDownloadManager#getDownloadUri(long)
     */
    public String getDownloadPath(long downloadId) {
        DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId);
        Cursor c = downloadManager.query(query);
        if (c != null) {
            try {
                if (c.moveToFirst()) {
                    return c.getString(c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI));
                }
            } finally {
                c.close();
            }
        }
        return null;
    }

    /**
     * 獲取儲存檔案的地址
     *
     * @param downloadId an ID for the download, unique across the system.
     *                   This ID is used to make future calls related to this download.
     * @see FileDownloadManager#getDownloadPath(long)
     */
    public Uri getDownloadUri(long downloadId) {
        return downloadManager.getUriForDownloadedFile(downloadId);
    }

    public DownloadManager getDownloadManager() {
        return downloadManager;
    }

    /**
     * 獲取下載狀態
     *
     * @param downloadId an ID for the download, unique across the system.
     *                   This ID is used to make future calls related to this download.
     * @return int
     * @see DownloadManager#STATUS_PENDING
     * @see DownloadManager#STATUS_PAUSED
     * @see DownloadManager#STATUS_RUNNING
     * @see DownloadManager#STATUS_SUCCESSFUL
     * @see DownloadManager#STATUS_FAILED
     */
    public int getDownloadStatus(long downloadId) {
        DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId);
        Cursor c = downloadManager.query(query);
        if (c != null) {
            try {
                if (c.moveToFirst()) {
                    return c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
                }
            } finally {
                c.close();
            }
        }
        return -1;
    }
}

app的檢測安裝的實現:

public class DownLoadApk {
    public static final String TAG = DownLoadApk.class.getSimpleName();

    public static void download(Context context, String url, String title,final String appName) {
        // 獲取儲存ID
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
        long downloadId =sp.getLong(DownloadManager.EXTRA_DOWNLOAD_ID,-1L);
        if (downloadId != -1L) {
            FileDownloadManager fdm = FileDownloadManager.getInstance(context);
            int status = fdm.getDownloadStatus(downloadId);
            if (status == DownloadManager.STATUS_SUCCESSFUL) {
                //啟動更新介面
                Uri uri = fdm.getDownloadUri(downloadId);
                if (uri != null) {
                    if (compare(getApkInfo(context, uri.getPath()), context)) {
                        startInstall(context, uri);
                        return;
                    } else {
                        fdm.getDownloadManager().remove(downloadId);
                    }
                }
                start(context, url, title,appName);
            } else if (status == DownloadManager.STATUS_FAILED) {
                start(context, url, title,appName);
            } else {
                Log.d(TAG, "apk is already downloading");
            }
        } else {
            start(context, url, title,appName);
        }
    }

    private static void start(Context context, String url, String title,String appName) {
        long id = FileDownloadManager.getInstance(context).startDownload(url,
                title, "下載完成後點選開啟",appName);
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
        sp.edit().putLong(DownloadManager.EXTRA_DOWNLOAD_ID,id).commit();
        Log.d(TAG, "apk start download " + id);
    }

    public static void startInstall(Context context, Uri uri) {
        Intent install = new Intent(Intent.ACTION_VIEW);
        install.setDataAndType(uri, "application/vnd.android.package-archive");
        install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(install);
    }


    /**
     * 獲取apk程式資訊[packageName,versionName...]
     *
     * @param context Context
     * @param path    apk path
     */
    private static PackageInfo getApkInfo(Context context, String path) {
        PackageManager pm = context.getPackageManager();
        PackageInfo info = pm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
        if (info != null) {
            return info;
        }
        return null;
    }


    /**
     * 下載的apk和當前程式版本比較
     *
     * @param apkInfo apk file's packageInfo
     * @param context Context
     * @return 如果當前應用版本小於apk的版本則返回true
     */
    private static boolean compare(PackageInfo apkInfo, Context context) {
        if (apkInfo == null) {
            return false;
        }
        String localPackage = context.getPackageName();
        if (apkInfo.packageName.equals(localPackage)) {
            try {
                PackageInfo packageInfo = context.getPackageManager().getPackageInfo(localPackage, 0);
                if (apkInfo.versionCode > packageInfo.versionCode) {
                    return true;
                }
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
}

上面的程式碼可知:我們通過獲取當前app的資訊來比較是否需要下載和是否立即安裝。第一次下載把downloadId儲存到本地,使用者下次進來的時候,取出儲存的downloadId,然後通過downloadId來獲取下載的狀態資訊。如果下載失敗,則重新下載並且把downloadId存起來。如果下載成功,則判斷本地的apk的包名是否和當前程式是相同的,並且本地apk的版本號大於當前程式的版本,如果都滿足則直接啟動安裝程式。

監聽app是否安裝完成

public class ApkInstallReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if(intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)){
              long downloadApkId =intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
              installApk(context, downloadApkId);
        }
    }

    /**
     * 安裝apk
     */
    private void installApk(Context context,long downloadApkId) {
        // 獲取儲存ID
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
        long downId =sp.getLong(DownloadManager.EXTRA_DOWNLOAD_ID,-1L);
        if(downloadApkId == downId){
            DownloadManager downManager= (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
            Uri downloadFileUri = downManager.getUriForDownloadedFile(downloadApkId);
            if (downloadFileUri != null) {
            Intent install= new Intent(Intent.ACTION_VIEW);
            install.setDataAndType(downloadFileUri, "application/vnd.android.package-archive");
            install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(install);
            }else{
                Toast.makeText(context, "下載失敗", Toast.LENGTH_SHORT).show();
            }
        }
    }
}

DownloadManager下載完成後會發出一個廣播 android.intent.action.DOWNLOAD_COMPLETE 新建一個廣播接收者即可:

清單配置:

先新增網路下載的許可權:

 <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

再新增靜態廣播:

 <receiver android:name=".ApkInstallReceiver">
            <intent-filter>
                <action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
            </intent-filter>
        </receiver>

使用HttpUrlConnection下載

這種情況下載的話我們就不需要考慮id的問題,因為是直接在專案中下載,所以我們就是一個網路下載的過程,並且使用ProgressDialog顯示下載資訊及進度更新就ok了。

public class AppInnerDownLoder {
    public final static String SD_FOLDER = Environment.getExternalStorageDirectory()+ "/VersionChecker/";
    private static final String TAG = AppInnerDownLoder.class.getSimpleName();

    /**
     * 從伺服器中下載APK
     */
    @SuppressWarnings("unused")
    public static void downLoadApk(final Context mContext,final String downURL,final String appName ) {

        final ProgressDialog pd; // 進度條對話方塊
        pd = new ProgressDialog(mContext);
        pd.setCancelable(false);// 必須一直下載完,不可取消
        pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        pd.setMessage("正在下載安裝包,請稍後");
        pd.setTitle("版本升級");
        pd.show();
        new Thread() {
            @Override
            public void run() {
                try {
                    File file = downloadFile(downURL,appName, pd);
                    sleep(3000);
                    installApk(mContext, file);
                    // 結束掉進度條對話方塊
                    pd.dismiss();
                } catch (Exception e) {
                    pd.dismiss();

                }
            }
        }.start();
    }

    /**
     * 從伺服器下載最新更新檔案
     * 
     * @param path
     *            下載路徑
     * @param pd
     *            進度條
     * @return
     * @throws Exception
     */
    private static File downloadFile(String path,String appName ,ProgressDialog pd) throws Exception {
        // 如果相等的話表示當前的sdcard掛載在手機上並且是可用的
        if (Environment.MEDIA_MOUNTED.equals(Environment
                .getExternalStorageState())) {
            URL url = new URL(path);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(5000);
            // 獲取到檔案的大小
            pd.setMax(conn.getContentLength());
            InputStream is = conn.getInputStream();
            String fileName = SD_FOLDER
                     + appName+".apk";
            File file = new File(fileName);
            // 目錄不存在建立目錄
            if (!file.getParentFile().exists())
                file.getParentFile().mkdirs();
            FileOutputStream fos = new FileOutputStream(file);
            BufferedInputStream bis = new BufferedInputStream(is);
            byte[] buffer = new byte[1024];
            int len;
            int total = 0;
            while ((len = bis.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
                total += len;
                // 獲取當前下載量
                pd.setProgress(total);
            }
            fos.close();
            bis.close();
            is.close();
            return file;
        } else {
            throw new IOException("未發現有SD卡");
        }
    }

    /**
     * 安裝apk
     */
    private static void installApk(Context mContext, File file) {
        Uri fileUri = Uri.fromFile(file);
        Intent it = new Intent();
        it.setAction(Intent.ACTION_VIEW);
        it.setDataAndType(fileUri, "application/vnd.android.package-archive");
        it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);// 防止打不開應用
        mContext.startActivity(it);
    }

    /**
     * 獲取應用程式版本(versionName)
     * 
     * @return 當前應用的版本號
     */

    private static double getLocalVersion(Context context) {
        PackageManager manager = context.getPackageManager();
        PackageInfo info = null;
        try {
            info = manager.getPackageInfo(context.getPackageName(), 0);
        } catch (NameNotFoundException e) {
            Log.e(TAG, "獲取應用程式版本失敗,原因:" + e.getMessage());
            return 0.0;
        }

        return Double.valueOf(info.versionName);
    }
    /** 
     * byte(位元組)根據長度轉成kb(千位元組)和mb(兆位元組) 
     *  
     * @param bytes 
     * @return 
     */  
    public static String bytes2kb(long bytes) {  
        BigDecimal filesize = new BigDecimal(bytes);  
        BigDecimal megabyte = new BigDecimal(1024 * 1024);  
        float returnValue = filesize.divide(megabyte, 2, BigDecimal.ROUND_UP)  
                .floatValue();  
        if (returnValue > 1)  
            return (returnValue + "MB");  
        BigDecimal kilobyte = new BigDecimal(1024);  
        returnValue = filesize.divide(kilobyte, 2, BigDecimal.ROUND_UP)  
                .floatValue();  
        return (returnValue + "KB");  
    }  
}

基本上具體的程式碼就寫完了,但是說如果停止了下載管理程式 呼叫dm.enqueue(req);就會上面的錯誤,從而程式閃退.

所以在使用該元件的時候,需要判斷該元件是否可用:

    private boolean canDownloadState() {
        try {
            int state = this.getPackageManager().getApplicationEnabledSetting("com.android.providers.downloads");
            if (state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED
                    || state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
                    || state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
                return false;
            }

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

可以通過如下程式碼進入 啟用/禁用 下載管理 介面:

 String packageName = "com.android.providers.downloads";
    Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    intent.setData(Uri.parse("package:" + packageName));
    startActivity(intent);

總結

本文意在講解app的更新邏輯以及不同的表現形式的處理附帶的介紹了使用nodejs寫一個簡單的api介面,重點是如何使用DownloadManager來實現apk的下載更新安裝,順帶講一下retrofit+rxjava的使用以及如何監聽app是否下載完成。

DownloadManager的使用概括:

  1. 構建下載請求:

    new DownloadManager.Request(url)
  2. 設定請求屬性

    request.setXXX()
  3. 呼叫downloadmanager物件的enqueue方法進行下載,此方法返回一個編號用於標示此下載任務:

    downManager = (DownloadManager)getSystemService(Context.DOWNLOAD_SERVICE);
    
    id= downManager.enqueue(request);
  4. DownManager會對所有的現在任務進行儲存管理,那麼我們如何獲取這些資訊呢?這個時候就要用到DownManager.Query物件,通過此物件,我們可以查詢所有下載任務資訊。

    setFilterById(long… ids):根據任務編號查詢下載任務資訊

    setFilterByStatus(int flags):根據下載狀態查詢下載任務

  5. 如果想取消下載,則可以呼叫remove方法完成,此方法可以將下載任務和已經下載的檔案同時刪除:

    downManager.remove(id);

好了具體的都講的差不多了,本文以同步到我的github

後記

鑑於版本更新沒有對6.0的許可權和7.0的FileProvider做適配,導致6.0和7.0的安裝失敗或者7.0直接crash,本文將不再解釋,自行處理這裡提供一個已經適配的demo下載