Android 從零開始擼一個(gè)應(yīng)用內(nèi)更新Demo

對(duì)于Android app來說,應(yīng)用內(nèi)更新幾乎成了一個(gè)標(biāo)配的功能了。原理其實(shí)不難,今天我們就從零開始擼一個(gè)自己的應(yīng)用內(nèi)更新的demo出來。

先看看最終實(shí)現(xiàn)的效果:

Picture

上圖的效果,稍微將功能拆分一下,可以總結(jié)為以下幾點(diǎn)。

1\. 檢查更新;
2\. 最新apk下載;
3\. apk下載成功后應(yīng)用內(nèi)跳轉(zhuǎn)安裝;

1.檢查更新

為了檢驗(yàn)檢查更新的效果,我們需要一個(gè)tomcat服務(wù)器。至于tomcat怎么搭建,這里就不花篇幅去講了,網(wǎng)上資料還是很多的。
Tomcat部署完成后,在Tomcat ROOT目錄上新建一個(gè)本次demo的目錄,并且將新版的apk文件和一個(gè)保存了新版apk相關(guān)信息的json文件放在demo目錄下。如下圖所示:

Picture

各位應(yīng)該已經(jīng)想到檢查更新的原理了,其實(shí)就是解析保存了新版apk信息的json文件,然后根據(jù)json中新版apk的版本信息來判斷當(dāng)前apk是否有可以更新。
我們這里模擬一個(gè)新版apk相關(guān)信息的json文件內(nèi)容。

{"data":{"content":"更新內(nèi)容如下:\n   1.xxxxxx;\n   2.xxxxxx;\n   3.xxxxxx;\n","id":"1","api_key":"android","version_code":"2","version_name":"1.0.2"},"msg":"獲取成功","status":1}

可以看到,對(duì)于檢查更新來說,最重要的幾個(gè)信息都包含在data字段中,包括了更新內(nèi)容,新版apk版本號(hào),新版apk版本名稱等。當(dāng)然,根據(jù)實(shí)際需求,這個(gè)json可能會(huì)有所不同,具體項(xiàng)目中可以做一些修改,屆時(shí)解析的時(shí)候稍作改動(dòng)就好了。

接下來完成檢查更新相關(guān)代碼:

    /**
     * 檢查更新
     */
    public void checkUpdate(OnCheckUpdateListener onCheckUpdateListener) {
        mOnCheckUpdateListener = onCheckUpdateListener;
        HttpUtils.sendOkHttpRequest(mUpdateHelper.getNewestApkVersionInfoUrl(), new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                ToastUtils.showToast("check update failed.");
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String strJson = response.body().string();
                Log.e("onResponse", "response.body().string() = " + strJson);
                if (parseJson(strJson).getVersionCode() > AppUtils.getAppVersionCode()) {
                    sendMessage(MSG_ON_FIND_NEW_VERSION, parseJson(strJson));
                } else {
                    sendMessage(MSG_ON_NEWEST, null);
                }
            }
        });
    }

    /**
     * 解析json數(shù)據(jù)
     *
     * @param jsonData json數(shù)據(jù)
     *                 {
     *                 "data": {
     *                 "content": "更新內(nèi)容如下:1.xxxxxx;/n 2.xxxxxx;/n 3.xxxxxx;/n",
     *                 "id": "1",
     *                 "api_key": "update test",
     *                 "version_code": "2"
     *                 },
     *                 "msg": "獲取成功",
     *                 "status": 1
     *                 }
     * @return dataBean
     */
    private DataBean parseJson(String jsonData) {
        DataBean dataBean = new DataBean();

        try {
            JSONObject jsonObject = new JSONObject(jsonData);
            JSONObject dataObject = jsonObject.getJSONObject("data");
            dataBean.setContent(dataObject.getString("content"));
            dataBean.setId(dataObject.getInt("id"));
            dataBean.setApiKey(dataObject.getString("api_key"));
            dataBean.setVersionCode(dataObject.getInt("version_code"));
            dataBean.setVersionName(dataObject.getString("version_name"));
            //            Log.e("parseJson", "content " + dataObject.getString("content"));
            //            Log.e("parseJson", "id " + dataObject.getInt("id"));
            //            Log.e("parseJson", "api_key " + dataObject.getString("api_key"));
            //            Log.e("parseJson", "version_code " + dataObject.getInt("version_code"));
            //            Log.e("parseJson", "version_name " + dataObject.getString
            // ("version_name"));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataBean;
    }

2.最新apk文件下載

如何檢查更新的代碼搞定了,接下來是稍微麻煩一點(diǎn)的,就是下載apk文件。
由于下載文件可以作為一個(gè)單獨(dú)的功能,所以將下載文件這一塊單獨(dú)獨(dú)立出來作為一個(gè)子模塊來編寫。其實(shí)谷歌官方有提供專門的DownloadManager來下載文件,但是很多國(guó)內(nèi)的rom把DownloadManager閹割了。所以這里我們自己來擼一個(gè)DownloadManager,具體代碼如下,注釋還是比較清晰的,就不多做解釋了。

DownloadManager.java:

public class DownloadManager {
    private static volatile DownloadManager manager = null;

    private DownloadTask downloadTask;

    private String mFileName;

    private String mFileParentPath;

    private DownloadManager() {
        mFileParentPath = DownloadHelper.getDownloadParentFilePath();
    }

    public static DownloadManager getInstance() {
        if (manager == null) {
            synchronized (DownloadManager.class) {
                if (manager == null) {
                    manager = new DownloadManager();
                }
            }
        }
        return manager;
    }

    /**
     * 開啟下載任務(wù)
     *
     * @param url                下載鏈接
     * @param onDownloadListener onDownloadListener
     */
    public void startDownload(String url, OnDownloadListener onDownloadListener) {
        startDownload(url, DownloadHelper.getDownloadParentFilePath(), DownloadHelper
                        .getUrlFileName(url)
                , onDownloadListener);
    }

    /**
     * 開啟下載任務(wù)
     *
     * @param url                下載鏈接
     * @param fileName           指定下載文件名
     * @param onDownloadListener onDownloadListener
     */
    public void startDownload(String url, @NonNull String fileName, OnDownloadListener
            onDownloadListener) {
        startDownload(url, DownloadHelper.getDownloadParentFilePath(), fileName,
                onDownloadListener);
    }

    /**
     * 開啟下載任務(wù)
     *
     * @param url                下載鏈接
     * @param fileParentPath     指定下載文件目錄
     * @param fileName           指定下載文件名
     * @param onDownloadListener onDownloadListener
     */
    public void startDownload(String url, @NonNull String fileParentPath, @NonNull String fileName,
                              OnDownloadListener onDownloadListener) {
        if (StringUtils.isEmpty(fileParentPath))
            fileParentPath = DownloadHelper.getDownloadParentFilePath();

        if (StringUtils.isEmpty(fileName))
            fileName = DownloadHelper.getUrlFileName(url);

        mFileParentPath = fileParentPath;
        mFileName = fileName;

        if (downloadTask == null) {
            downloadTask = new DownloadTask(onDownloadListener);
            downloadTask.execute(url, fileParentPath, fileName);
            downloadTask.setOnDownloadTaskFinshedListener(new DownloadTask
                    .OnDownloadTaskFinshedListener() {
                @Override
                public void onFinished() {
                    downloadTask = null;
                }

                @Override
                public void onCanceled() {
                    //下載任務(wù)取消,刪除已下載的文件
                    clearCacheFile(getDownloadFilePath());
                }

                @Override
                public void onException() {
                    //下載任務(wù)異常,刪除已下載的文件
                    clearCacheFile(getDownloadFilePath());
                }
            });
        }
    }

    /**
     * 暫停下載任務(wù)
     */
    public void pauseDownload() {
        if (downloadTask != null) {
            downloadTask.pauseDownload();
        }
    }

    /**
     * 取消下載任務(wù)
     */
    public void cancelDownload() {
        if (downloadTask != null) {
            downloadTask.cancelDownload();
        }
    }

    /**
     * 清除下載的全部cache文件
     */
    public void clearAllCacheFile() {
        if (StringUtils.isEmpty(mFileParentPath))
            return;

        FileUtils.delAllFile(mFileParentPath);
    }

    /**
     * 清除下載的cache文件
     *
     * @param filePath 要?jiǎng)h除文件的絕對(duì)路徑
     */
    public void clearCacheFile(String filePath) {
        if (StringUtils.isEmpty(filePath))
            return;

        FileUtils.delFile(filePath);
    }

    /**
     * 獲取下載文件的全路徑
     *
     * @return 下載文件的全路徑
     */
    public String getDownloadFilePath() {
        if (StringUtils.isEmpty(mFileParentPath) || StringUtils.isEmpty(mFileName))
            return null;

        return mFileParentPath + "/" + mFileName;
    }
}

DownloadTask.java

public class DownloadTask extends AsyncTask<String, Integer, Integer> {

    private static final int TYPE_SUCCESS = 0;
    private static final int TYPE_FAILED = 1;
    private static final int TYPE_PAUSED = 2;
    private static final int TYPE_CANCELED = 3;

    private OnDownloadListener mOnDownloadListener;
    private OnDownloadTaskFinshedListener mOnDownloadTaskFinshedListener;

    private boolean isCanceled = false;

    private boolean isPaused = false;

    private int lastProgress;

    private File mDownloadFile = null;

    private long mContentLength; // 記錄url下載文件的長(zhǎng)度

    public DownloadTask(OnDownloadListener onDownloadListener) {
        mOnDownloadListener = onDownloadListener;
    }

    public void setOnDownloadTaskFinshedListener(OnDownloadTaskFinshedListener
                                                         onDownloadTaskFinshedListener) {
        mOnDownloadTaskFinshedListener = onDownloadTaskFinshedListener;
    }

    @Override
    protected Integer doInBackground(String... params) {
        InputStream is = null;
        RandomAccessFile savedFile = null;
        try {

            long downloadLength = 0; // 記錄已下載的文件長(zhǎng)度
            String downloadUrl = params[0];
            String fileParentPath = params[1];
            String fileName = params[2];
            mDownloadFile = new File(fileParentPath, fileName);
            if (mDownloadFile.exists()) {
                downloadLength = mDownloadFile.length();
            }

            mContentLength = getContentLength(downloadUrl);
            if (mContentLength == 0) {
                return TYPE_FAILED;
            } else if (mContentLength == downloadLength) {
                // 已下載字節(jié)和文件總字節(jié)相等,說明已經(jīng)下載完成了
                return TYPE_SUCCESS;
            }
            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    // 斷點(diǎn)下載,指定從哪個(gè)字節(jié)開始下載
                    .addHeader("RANGE", "bytes=" + downloadLength + "-")
                    .url(downloadUrl)
                    .build();
            Response response = client.newCall(request).execute();
            if (response != null) {
                is = response.body().byteStream();
                savedFile = new RandomAccessFile(mDownloadFile, "rw");
                savedFile.seek(downloadLength); // 跳過已下載的字節(jié)
                byte[] b = new byte[1024];
                int total = 0;
                int len;
                while ((len = is.read(b)) != -1) {
                    if (isCanceled) {
                        return TYPE_CANCELED;
                    } else if (isPaused) {
                        return TYPE_PAUSED;
                    } else {
                        total += len;
                        savedFile.write(b, 0, len);
                        // 計(jì)算已下載的百分比
                        int progress = (int) ((total + downloadLength) * 100 / mContentLength);
                        publishProgress(progress);
                    }
                }
                response.body().close();
                return TYPE_SUCCESS;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
                if (savedFile != null) {
                    savedFile.close();
                }
                if (isCanceled &amp;&amp; mDownloadFile != null) {
                    mDownloadFile.delete();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return TYPE_FAILED;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        int progress = values[0];
        if (progress > lastProgress) {
            mOnDownloadListener.onProgress(progress);
            lastProgress = progress;
        }
    }

    @Override
    protected void onPostExecute(Integer status) {
        switch (status) {
            case TYPE_SUCCESS:
                if (mContentLength != mDownloadFile.length()) {
                    if (mOnDownloadListener != null)
                        mOnDownloadListener.onException();

                    //下載數(shù)據(jù)異常,告知downManager下載任務(wù)已失敗
                    if (mOnDownloadTaskFinshedListener != null)
                        mOnDownloadTaskFinshedListener.onException();
                } else {
                    if (mOnDownloadListener != null)
                        mOnDownloadListener.onSuccess();
                }
                break;
            case TYPE_FAILED:
                if (mOnDownloadListener != null)
                    mOnDownloadListener.onFailed();
                break;
            case TYPE_PAUSED:
                if (mOnDownloadListener != null)
                    mOnDownloadListener.onPaused();
                break;
            case TYPE_CANCELED:
                if (mOnDownloadListener != null)
                    mOnDownloadListener.onCanceled();

                if (mOnDownloadTaskFinshedListener != null)
                    mOnDownloadTaskFinshedListener.onCanceled();
            default:
                break;
        }

        if (mOnDownloadTaskFinshedListener != null)
            mOnDownloadTaskFinshedListener.onFinished();
    }

    /**
     * 暫停下載任務(wù)
     */
    public void pauseDownload() {
        isPaused = true;
    }

    /**
     * 取消下載任務(wù)
     */
    public void cancelDownload() {
        isCanceled = true;
    }

    /**
     * 獲取下載文件長(zhǎng)度
     * @param downloadUrl 下載文件url
     * @return 下載文件長(zhǎng)度
     * @throws IOException IOException
     */
    private long getContentLength(String downloadUrl) throws IOException {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(downloadUrl)
                .build();
        Response response = client.newCall(request).execute();
        if (response != null &amp;&amp; response.isSuccessful()) {
            long contentLength = response.body().contentLength();
            response.close();
            return contentLength;
        }
        return 0;
    }

    public interface OnDownloadTaskFinshedListener {
        /**
         * 下載任務(wù)已結(jié)束
         */
        void onFinished();

        /**
         * 下載任務(wù)已取消
         */
        void onCanceled();

        /**
         * 下載文件異常,不是完整的文件或者文件包異常
         */
        void onException();
    }
}

3.apk下載成功后應(yīng)用內(nèi)跳轉(zhuǎn)安裝

在7.0以前,通過apk的uri跳轉(zhuǎn)安裝就可以了。7.0以后由于StrictMode API 政策,需要通過FileProvider來安裝apk文件,使用步驟也比較簡(jiǎn)單。

第一步:在AndroidManifest.xml清單文件中注冊(cè)provider
  • exported:必須為false,true會(huì)報(bào)安全異常。

  • grantUriPermissions:true,授予 URI 臨時(shí)訪問權(quán)限。

  • authorities 組件標(biāo)識(shí)

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="${applicationId}.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths"/>
</provider>
第二步:指定共享的目錄

在res資源目錄下新建一個(gè)xml目錄,并新建一個(gè)名字和上一步指定的resource文件相同名字的xml文件,這里是:file_paths。 file_path.xml的內(nèi)容如下:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path path="" name="updateDemo" />
</paths>

< files-path />代表根目錄: Context.getFilesDir()
< external-path />代表根目錄: Environment.getExternalStorageDirectory()
< cache-path />代表根目錄: getCacheDir()
external-path path=""代表根目錄,也就是說你可以向其它的應(yīng)用共享根目錄及其子目錄下任何一個(gè)文件了。

第三步:使用FileProvider
    /**
     * 安裝 apk
     *
     * @param apkPath apk全路徑
     */
    public void installApk(String apkPath) {
        if (StringUtils.isEmpty(apkPath)) {
            Log.e("tag", "apkPath is null.");
            return;
        }

        File file = new File(apkPath);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        // 由于沒有在Activity環(huán)境下啟動(dòng)Activity,設(shè)置下面的標(biāo)簽
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (Build.VERSION.SDK_INT >= 24) { //判斷版本是否在7.0以上
            //參數(shù)1 上下文, 參數(shù)2 Provider主機(jī)地址 和配置文件中保持一致  參數(shù)3 共享的文件
            Uri apkUri = FileProvider.getUriForFile(AppUtils.getContext(), BuildConfig
                    .APPLICATION_ID + ".fileProvider", file);
            //添加這一句表示對(duì)目標(biāo)應(yīng)用臨時(shí)授權(quán)該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");
        }
        AppUtils.getContext().startActivity(intent);
    }

接下來完成UpdateManager的全部代碼:

package com.zyw.horrarndoo.updatedemo.update;

import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.support.v4.content.FileProvider;
import android.util.Log;

import com.zyw.horrarndoo.updatedemo.BuildConfig;
import com.zyw.horrarndoo.updatedemo.bean.DataBean;
import com.zyw.horrarndoo.updatedemo.download.DownloadHelper;
import com.zyw.horrarndoo.updatedemo.download.DownloadManager;
import com.zyw.horrarndoo.updatedemo.download.OnDownloadListener;
import com.zyw.horrarndoo.updatedemo.net.HttpUtils;
import com.zyw.horrarndoo.updatedemo.utils.AppUtils;
import com.zyw.horrarndoo.updatedemo.utils.SpUtils;
import com.zyw.horrarndoo.updatedemo.utils.StringUtils;
import com.zyw.horrarndoo.updatedemo.utils.ToastUtils;

import org.json.JSONObject;

import java.io.File;
import java.io.IOException;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Response;

import static com.zyw.horrarndoo.updatedemo.constant.Constant.SP_KEY_CACHE_APK_VERSION_CODE;
import static com.zyw.horrarndoo.updatedemo.constant.Constant.SP_KEY_CACHE_VALID_TIME;

/**
 * Created by Horrarndoo on 2018/2/1.
 * <p>
 */

public class UpdateManager {
    private static volatile UpdateManager manager = null;

    private static final int MSG_ON_START = 1;
    private static final int MSG_ON_PROGRESS = 2;
    private static final int MSG_ON_DOWNLOAD_FINISH = 3;
    private static final int MSG_ON_FAILED = 4;
    private static final int MSG_ON_CANCLE = 5;
    private static final int MSG_ON_FIND_NEW_VERSION = 6;
    private static final int MSG_ON_NEWEST = 7;
    private static final int MSG_ON_UPDATE_EXCEPTION = 8;

    private DownloadManager mDownloadManager;

    private OnUpdateListener mOnUpdateListener;
    private OnCheckUpdateListener mOnCheckUpdateListener;

    private int mNewestVersionCode;
    private String mNewestVersionName;
    private String mNewVersionContent;

    /**
     * 最后一次保存cache的時(shí)間
     */
    private long mLastCacheSaveTime = 0;

    private UpdateManager() {
        mDownloadManager = DownloadManager.getInstance();
    }

    /**
     * 獲取updateManager實(shí)例
     *
     * @return updateManager實(shí)例
     */
    public static UpdateManager getInstance() {
        if (manager == null) {
            synchronized (UpdateManager.class) {
                if (manager == null) {
                    manager = new UpdateManager();
                }
            }
        }
        return manager;
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case MSG_ON_START:
                    if (mOnUpdateListener != null)
                        mOnUpdateListener.onStartUpdate();
                    break;

                case MSG_ON_PROGRESS:
                    if (mOnUpdateListener != null)
                        mOnUpdateListener.onProgress((Integer) msg.obj);
                    break;

                case MSG_ON_DOWNLOAD_FINISH:
                    if (mOnUpdateListener != null)
                        mOnUpdateListener.onApkDownloadFinish((String) msg.obj);
                    installApk((String) msg.obj);
                    break;

                case MSG_ON_FAILED:
                    if (mOnUpdateListener != null)
                        mOnUpdateListener.onUpdateFailed();
                    break;

                case MSG_ON_CANCLE:
                    if (mOnUpdateListener != null)
                        mOnUpdateListener.onUpdateCanceled();
                    break;

                case MSG_ON_UPDATE_EXCEPTION:
                    if (mOnUpdateListener != null)
                        mOnUpdateListener.onUpdateException();
                    break;

                case MSG_ON_FIND_NEW_VERSION:
                    DataBean dataBean = (DataBean) msg.obj;

                    mNewestVersionCode = dataBean.getVersionCode();
                    mNewestVersionName = dataBean.getVersionName();
                    mNewVersionContent = dataBean.getContent();

                    if (mOnCheckUpdateListener != null)
                        mOnCheckUpdateListener.onFindNewVersion(mNewestVersionName,
                                mNewVersionContent);
                    break;

                case MSG_ON_NEWEST:
                    if (mOnCheckUpdateListener != null)
                        mOnCheckUpdateListener.onNewest();
                    break;
            }
        }
    };

    /**
     * 檢查更新
     *
     * @param apkInfoUrl            服務(wù)器端保存新版apk相關(guān)信息json的url
     * @param onCheckUpdateListener onCheckUpdateListener
     */
    public void checkUpdate(String apkInfoUrl, OnCheckUpdateListener onCheckUpdateListener) {
        mOnCheckUpdateListener = onCheckUpdateListener;
        HttpUtils.sendOkHttpRequest(apkInfoUrl, new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                ToastUtils.showToast("check update failed.");
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String strJson = response.body().string();
                Log.e("onResponse", "response.body().string() = " + strJson);
                if (parseJson(strJson).getVersionCode() > AppUtils.getAppVersionCode()) {
                    //最后一次緩存的時(shí)間超過緩存文件有效期,或者最后一次緩存的apk不是最新版本的apk,刪除緩存apk
                    if ((System.currentTimeMillis() - mLastCacheSaveTime > getCacheSaveValidTime())
                            || (getCacheApkVersionCode() != parseJson(strJson).getVersionCode())) {
                        clearCacheApkFile();
                        setCacheApkVersionCode(parseJson(strJson).getVersionCode());
                    }
                    sendMessage(MSG_ON_FIND_NEW_VERSION, parseJson(strJson));
                } else {
                    sendMessage(MSG_ON_NEWEST, null);
                    //當(dāng)前已經(jīng)是最新版本APK,清除本地已經(jīng)緩存的apk安裝包
                    clearCacheApkFile();
                }
            }
        });
    }

    /**
     * 開始更新App
     * <p>
     * 此時(shí)開始正式下載更新Apk
     *
     * @param apkUrl           服務(wù)端最新apk文件url
     * @param onUpdateListener onUpdateListener
     */
    public void startToUpdate(String apkUrl, OnUpdateListener onUpdateListener) {
        mOnUpdateListener = onUpdateListener;

        if (StringUtils.isEmpty(mNewestVersionName) || mNewestVersionCode == 0)
            return;

        downloadNewestApkFile(apkUrl, mNewestVersionCode, mNewestVersionName);
    }

    /**
     * 設(shè)置緩存文件有效時(shí)間,單位:秒
     * <p>
     * 默認(rèn)緩存有效期為7天
     *
     * @param cacheValidTime 緩存文件有效時(shí)間
     */
    public void setCacheSaveValidTime(long cacheValidTime) {
        SpUtils.putLong(SP_KEY_CACHE_VALID_TIME, cacheValidTime);
    }

    /**
     * 獲取緩存文件有效時(shí)間,單位:秒
     * <p>
     * 默認(rèn)緩存有效期為7天
     *
     * @return 緩存文件有效時(shí)間
     */
    public long getCacheSaveValidTime() {
        return SpUtils.getLong(SP_KEY_CACHE_VALID_TIME, 60 * 60 * 24 * 7);
    }

    /**
     * 設(shè)置緩存文件版本號(hào)
     *
     * @param versionCode cacheApk版本號(hào)
     */
    public void setCacheApkVersionCode(int versionCode) {
        SpUtils.putInt(SP_KEY_CACHE_APK_VERSION_CODE, versionCode);
    }

    /**
     * 獲取緩存文件版本號(hào)
     *
     * @return 緩存文件版本號(hào)
     */
    public int getCacheApkVersionCode() {
        return SpUtils.getInt(SP_KEY_CACHE_APK_VERSION_CODE, 0);
    }

    /**
     * 取消更新
     */
    public void cancleUpdate() {
        //保留下載已完成的部分apk cache文件,cache文件最多保留7天
        mDownloadManager.pauseDownload();
    }

    /**
     * 清除已下載的APK緩存
     */
    public void clearCacheApkFile() {
        Log.e("tag", "清除所有的apk文件");
        mDownloadManager.clearAllCacheFile();
    }

    /**
     * 下載最新版本的APK文件
     *
     * @param url               服務(wù)端最新apk文件url
     * @param newestVersionCode 最新版本APK版本號(hào)
     * @param newestVersionName 最新版本APK版本名稱
     */
    private void downloadNewestApkFile(String url, int newestVersionCode, String
            newestVersionName) {
        String apkFileName = getApkNameWithVersionName(DownloadHelper.getUrlFileName(url),
                newestVersionName);

        sendMessage(MSG_ON_START, null);

        mDownloadManager.startDownload(url, apkFileName, new
                OnDownloadListener() {
                    @Override
                    public void onException() {
                        sendMessage(MSG_ON_UPDATE_EXCEPTION, null);
                    }

                    @Override
                    public void onProgress(int progress) {
                        sendMessage(MSG_ON_PROGRESS, progress);
                    }

                    @Override
                    public void onSuccess() {
                        mLastCacheSaveTime = System.currentTimeMillis();
                        sendMessage(MSG_ON_DOWNLOAD_FINISH, mDownloadManager.getDownloadFilePath());
                    }

                    @Override
                    public void onFailed() {
                        mLastCacheSaveTime = System.currentTimeMillis();
                        sendMessage(MSG_ON_FAILED, null);
                    }

                    @Override
                    public void onPaused() {
                        mLastCacheSaveTime = System.currentTimeMillis();
                        //取消升級(jí)時(shí),調(diào)用download pause,保留已下載的部分apk文件
                        sendMessage(MSG_ON_CANCLE, null);
                    }

                    @Override
                    public void onCanceled() {
                        //為了保證斷點(diǎn)續(xù)傳,升級(jí)時(shí),調(diào)用download pause,不使用cancle,onCancle不會(huì)被調(diào)用
                        mLastCacheSaveTime = System.currentTimeMillis();
                        sendMessage(MSG_ON_CANCLE, null);
                    }
                });
    }

    private void sendMessage(int msgWhat, Object o) {
        Message msg = Message.obtain();
        msg.what = msgWhat;
        msg.obj = o;
        mHandler.sendMessage(msg);
    }

    /**
     * 解析json數(shù)據(jù)
     *
     * @param jsonData json數(shù)據(jù)
     * @return dataBean
     */
    private DataBean parseJson(String jsonData) {
        ...
        return dataBean;
    }

    /**
     * 獲取帶版本名稱的apk文件名
     *
     * @param apkName apk原名
     * @return 帶版本名稱的apk文件名
     */
    private String getApkNameWithVersionName(String apkName, String versionName) {
        if (StringUtils.isEmpty(apkName))
            return apkName;

        apkName = apkName.substring(apkName.lastIndexOf("/") + 1, apkName.indexOf("" +
                ".apk"));
        Log.e("tag", "newApkName = " + apkName + "_v" + versionName + ".apk");
        return apkName + "_v" + versionName + ".apk";
    }

    /**
     * 安裝 apk
     *
     * @param apkPath apk全路徑
     */
    public void installApk(String apkPath) {
        ...
    }
}

最后在Activity中調(diào)用UpdateManager就可以簡(jiǎn)單的檢查更新我們的apk啦,不過有一點(diǎn)稍微要注意下,就是運(yùn)行時(shí)權(quán)限的獲取,如果6.0以上的手機(jī)沒有獲取sd卡權(quán)限的話,我們的程序是無法正常運(yùn)行的,所以如果用戶沒有給予權(quán)限的話,就退出程序并給出提示。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private UpdateManager mUpdateManager;

    private ProgressDialog mProgressDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        init();
    }

    private void init() {
        ...

        initPermission();

        mUpdateManager = UpdateManager.getInstance();
    }

    private void initProgressDialog() {
        ...
    }

    private void initPermission() {
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission
                .WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission
                    .WRITE_EXTERNAL_STORAGE}, 1);
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_check_update:
                mUpdateManager.checkUpdate(Constant.VERSION_INFO_URL, new OnCheckUpdateListener() {
                    @Override
                    public void onFindNewVersion(String versionName, String newVersionContent) {
                        String content = "最新版: V" + versionName + "\n" + newVersionContent;
                        buildNewVersionDialog(content);
                    }

                    @Override
                    public void onNewest() {
                        showToast("app is newest version.");
                        dismissProgressDialog();
                    }
                });

                break;
            case R.id.btn_clear_apk:
                mUpdateManager.clearCacheApkFile();
                break;
            default:
                break;
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        switch (requestCode) {
            case 1:
                if (grantResults.length > 0 &amp;&amp; grantResults[0] != PackageManager
                        .PERMISSION_GRANTED) {
                    Toast.makeText(this, "拒絕權(quán)限將無法使用程序", Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
        }
    }

    private void dismissProgressDialog() {
        mProgressDialog.setProgress(0);
        if (mProgressDialog.isShowing())
            mProgressDialog.dismiss();
    }

    /**
     * 創(chuàng)建發(fā)現(xiàn)新版本apk alert dialog
     *
     * @param message dialog顯示消息
     */
    private void buildNewVersionDialog(String message) {
        AlertDialog dialog = new AlertDialog.Builder(this)
                .setTitle("發(fā)現(xiàn)新版本")
                .setIcon(R.mipmap.ic_launcher)
                .setMessage(message)
                .setPositiveButton("更新", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                        mUpdateManager.startToUpdate(Constant.APK_URL, mOnUpdateListener);
                    }
                })
                .setNegativeButton("取消", null)
                .create();
        dialog.show();
    }

    private OnUpdateListener mOnUpdateListener = new OnUpdateListener() {
        @Override
        public void onStartUpdate() {
            mProgressDialog.show();
        }

        @Override
        public void onProgress(int progress) {
            mProgressDialog.setProgress(progress);
        }

        @Override
        public void onApkDownloadFinish(String apkPath) {
            showToast("newest apk download finish. apkPath: " + apkPath);
            Log.e("tag", "newest apk download finish. apkPath: " + apkPath);
            dismissProgressDialog();
            //所有的更新全部在updateManager中完成,Activity在這里只是做一些界面上的處理
        }

        @Override
        public void onUpdateFailed() {
            showToast("update failed.");
            dismissProgressDialog();
        }

        @Override
        public void onUpdateCanceled() {
            showToast("update cancled.");
            dismissProgressDialog();
        }

        @Override
        public void onUpdateException() {
            showToast("update exception.");
            dismissProgressDialog();
        }
    };
}

至此已經(jīng)基本完成此次的應(yīng)用內(nèi)更新模塊了,用起來還是很簡(jiǎn)單的,UpdateManager只做更新相關(guān)判斷處理,DownloadManager則處理下載文件、緩存文件續(xù)傳及相關(guān)狀態(tài)處理,并且UpdateManager和DownloadManager都是單獨(dú)可以作為一個(gè)獨(dú)立模塊在實(shí)際項(xiàng)目中使用的。

最后附上源碼:https://github.com/Horrarndoo/UpdateDemo

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容