Android檢查版本升級(jí)應(yīng)該怎么做?

加菲貓.jpeg

demo演示:https://github.com/pzl237/UpgradeDemo

背景

今年年初項(xiàng)目終于上線,到目前為止發(fā)布了4個(gè)版本。經(jīng)歷了3.8節(jié),整體表現(xiàn)穩(wěn)定。在第三個(gè)版本我們加入了版本檢查升級(jí),發(fā)布第四版本,用戶就直接體驗(yàn)到了這個(gè)功能。

必要性

我們開(kāi)發(fā)一個(gè)APP,應(yīng)該是發(fā)布第一個(gè)版本之后,后續(xù)不斷的更新迭代?,F(xiàn)在大部分APP都是發(fā)布到各大應(yīng)用市場(chǎng)市場(chǎng),然后用戶去搜索我們的應(yīng)用并下載安裝。如果有新的版本,你不可能讓每個(gè)用戶去應(yīng)用市場(chǎng)重新下載新的版本,又或者用戶沒(méi)注意應(yīng)用市場(chǎng)的更新提醒導(dǎo)致沒(méi)有安裝最新版本等,所以我們有必要讓我們的應(yīng)用自己檢查是否有新的版本。

升級(jí)流程圖

在APP首頁(yè)自動(dòng)觸發(fā)向服務(wù)端請(qǐng)求最新的版本信息,如果服務(wù)端返回的版本信息中versionCode與當(dāng)前版本不一致,就彈出升級(jí)提示框讓用戶選擇。流程圖如下:

檢查更新流程圖.png

“立即更新”:直接啟動(dòng)下載
“稍后提醒”:什么都不處理
“忽略該版本”:當(dāng)前版本不再提醒,如有更新版本還是要提醒。例如:用戶版本是1.0,當(dāng)前檢查到的版本是2.0,用戶選擇了“忽略該版本”,則2.0的版本不再提示,到下個(gè)版本時(shí),仍然要提示版本更新。

實(shí)現(xiàn)

首先看下app module的build.gradle

每次發(fā)布一個(gè)新版本時(shí),一般都會(huì)修改versionCode以及versionName。

defaultConfig {
        applicationId "com.jemlin.app"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0.0"
        ...
    }

其中versionCode是整型,這里定義從1開(kāi)始,每次迭代一個(gè)版本就加1;versionName是字符類型,從1.0.0開(kāi)始,每次更新可以改為1.0.1、1.1.0、2.0.0等等。

接著判斷是否有升級(jí)版本

通過(guò)和后端定好的http api從服務(wù)器端請(qǐng)求最新版本的versionCode,然后與當(dāng)前版本的versionCode比較,如果服務(wù)端返回版本信息的versionCode大于當(dāng)前版本的versionCode,就說(shuō)明有新的版本需要更新。

1、http api檢查版本更新接口response數(shù)據(jù)格式(僅供參考):

{
    "data": {
        "downloadUrl": "http://a5.pc6.com/cx3/weixin.pc6.apk",
        "version": "1.0.1",
        "versionCode": 2,
        "versionDesc": "主要修改:\n1.增加多項(xiàng)新功能;\n2.修復(fù)已知bug。"
    },
    "errCode": 0,
    "errMsg": "",
    "success": true
}

其中,downloadUrl是最新版本的下載地址。

2、定義VersionInfo模型,用于GSon解析服務(wù)端返回的數(shù)據(jù):

public class VersionInfo {
    private int versionCode;
    private String version;
    private String downloadUrl;
    private String versionDesc;
    //......
}

3、如果有升級(jí)版本,隨時(shí)彈窗提示用戶。沒(méi)有升級(jí)版本,就不用提示。

忽略更新

我的做法是把用戶忽略更新的版本號(hào)versionCode存儲(chǔ)到sharePreference中,每次發(fā)現(xiàn)有升級(jí)版本時(shí),在給用戶提示之前,先取忽略版本號(hào)versionCode與最新版本號(hào)versionCode比較是否一樣,如果一樣就什么都不做,檢查更新結(jié)束;如果不一樣,還是照樣給用戶提示。

//取sp中保存的versionCode
int versionCode = mAppUpgradePersistent.getIgnoreUpgradeVersionCode(appContext);
if (versionCode == latestVersion.getVersionCode()) {
  //用戶之前已經(jīng)選擇"忽略該版本",不更新這個(gè)版本。
  Timber.d("[AppUpgradeManager] ignore upgrade version====");
  return;
}

稍后提醒

最簡(jiǎn)單,什么都不做。

立即更新

1、項(xiàng)目中我們直接使用系統(tǒng)提供的DownloadManager服務(wù),同時(shí)注冊(cè)兩個(gè)廣播:
下載完成廣播DownloadManager.ACTION_DOWNLOAD_COMPLETE以及
點(diǎn)擊下載通知欄廣播DownloadManager.ACTION_NOTIFICATION_CLICKED
代碼片段如下:

    public void init(Context context) {
        Timber.d("[AppUpgradeManager] init====");
        if (isInit) {
            return;
        }

        appContext = context.getApplicationContext();
        isInit = true;
        mAppUpgradePersistent = new AppUpgradePersistent();
        appContext.registerReceiver(downloaderReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
        appContext.registerReceiver(notificationClickReceiver, new IntentFilter(DownloadManager.ACTION_NOTIFICATION_CLICKED));
    }

    public void unInit() {
        Timber.d("[AppUpgradeManager] unInit====");
        if (!isInit) {
            return;
        }
        appContext.unregisterReceiver(downloaderReceiver);
        appContext.unregisterReceiver(notificationClickReceiver);
        isInit = false;
        mAppUpgradePersistent = null;
        appContext = null;
    }

2、如果可以的話,在用戶選擇立即更新之后,您的應(yīng)用應(yīng)該判斷當(dāng)前的網(wǎng)絡(luò)環(huán)境,如果是非wifi環(huán)境應(yīng)該彈窗提示用戶類似“您當(dāng)前使用的不是wifi,更新會(huì)產(chǎn)生一些網(wǎng)絡(luò)流量,是否繼續(xù)下載?”
代碼片段如下:

            // 非wifi網(wǎng)絡(luò)下,再次提示用戶是否繼續(xù)
            MaterialDialog.Builder builder = new MaterialDialog.Builder(activity);
            final MaterialDialog dialog = builder.title("流量提醒")
                    .theme(Theme.LIGHT)
                    .titleGravity(GravityEnum.CENTER)
                    .content("您當(dāng)前使用的不是wifi,更新會(huì)產(chǎn)生一些網(wǎng)絡(luò)流量,是否繼續(xù)下載?")
                    .positiveText("確定")
                    .negativeText("取消")
                    .onPositive(new MaterialDialog.SingleButtonCallback() {
                        @Override
                        public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
                            dialog.dismiss();
                            //TODO 
                        }
                    })
                    .onNegative(new MaterialDialog.SingleButtonCallback() {
                        @Override
                        public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
                            dialog.dismiss();
                            //TODO
                        }
                    })
                    .build();
            dialog.show();

3、真正調(diào)用DownloadManager下載前,我們可以判斷下本地是否已經(jīng)過(guò)了最新版本,有就直接啟動(dòng)安裝界面安裝;下載的不是最新版本,就直接刪除。
代碼片段如下:

        //先檢查本地是否已經(jīng)有需要升級(jí)版本的安裝包,如有就不需要再下載
        File targetApkFile = new File(downloadApkPath);
        if (targetApkFile.exists()) {
            PackageManager pm = appContext.getPackageManager();
            PackageInfo info = pm.getPackageArchiveInfo(downloadApkPath, PackageManager.GET_ACTIVITIES);
            if (info != null) {
                String versionCode = String.valueOf(info.versionCode);
                //比較已下載到本地的apk安裝包,與服務(wù)器上apk安裝包的版本號(hào)是否一致
                if (String.valueOf(latestVersion.getVersionCode()).equals(versionCode)) {
                    //彈出框提示用戶安裝
                    mHandler.obtainMessage(WHAT_ID_INSTALL_APK, downloadApkPath).sendToTarget();
                    return;
                }
            }
        }

        //要檢查本地是否有安裝包,有則刪除重新下
        File apkFile = new File(downloadApkPath);
        if (apkFile.exists()) {
            boolean isDelSuc = apkFile.delete();
        }

4、創(chuàng)建下載Reuqst,開(kāi)始下載。
代碼片段如下:

        Request task = new Request(Uri.parse(latestVersion.getDownloadUrl()));
        //定制Notification的樣式
        String title = "應(yīng)用名稱:" + latestVersion.getVersion();
        task.setTitle(title);
        task.setDescription(latestVersion.getVersionDesc());
       //如果我們希望下載的文件可以被系統(tǒng)的Downloads應(yīng)用掃描到并管理,我們需要調(diào)用Request對(duì)象的setVisibleInDownloadsUi方法,傳遞參數(shù)true
        task.setVisibleInDownloadsUi(true);
        //設(shè)置是否允許手機(jī)在漫游狀態(tài)下下載
        task.setAllowedOverRoaming(false);
        //限定在WiFi下進(jìn)行下載
        task.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE);
        task.setMimeType("application/vnd.android.package-archive");
        // 在通知欄通知下載中和下載完成
        // 下載完成后該Notification才會(huì)被顯示
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) {
            // 3.0(11)以后才有該方法
            //在下載過(guò)程中通知欄會(huì)一直顯示該下載的Notification,在下載完成后該Notification會(huì)繼續(xù)顯示,直到用戶點(diǎn)擊該Notification或者消除該Notification
            task.allowScanningByMediaScanner();
            task.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        }
        // 可能無(wú)法創(chuàng)建Download文件夾,如無(wú)sdcard情況,系統(tǒng)會(huì)默認(rèn)將路徑設(shè)置為/data/data/com.android.providers.downloads/cache/xxx.apk
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            String apkName = UpgradeHelper.downloadTempName(appContext.getPackageName());
            task.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, apkName);
        }
        downloadTaskId = downloader.enqueue(task);
        mAppUpgradePersistent.saveDownloadTaskId(appContext, downloadTaskId);

下載完成廣播

public void init(Context context)方法中已經(jīng)注冊(cè)了監(jiān)聽(tīng)下載完成廣播,一旦我們知道下載完成的時(shí)機(jī),就可以調(diào)用系統(tǒng)安裝界面安裝我們的APK啦~
切記,不可在廣播的onReceive中做耗時(shí)操作,時(shí)間不能超過(guò)10秒,否則將ANR卡死!
下載完成廣播定義如下:

    /**
     * 下載完成的廣播
     */
    class DownloadReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (downloader == null) {
                return;
            }
            long completeId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0);
            long downloadTaskId = mAppUpgradePersistent.getDownloadTaskId(context);
            if (completeId != downloadTaskId) {
                return;
            }

            Query query = new Query();
            query.setFilterById(downloadTaskId);
            Cursor cur = downloader.query(query);
            if (!cur.moveToFirst()) {
                return;
            }

            int columnIndex = cur.getColumnIndex(DownloadManager.COLUMN_STATUS);
            if (DownloadManager.STATUS_SUCCESSFUL == cur.getInt(columnIndex)) {
                String uriString = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
                mHandler.obtainMessage(WHAT_ID_INSTALL_APK, uriString).sendToTarget();
            } else {
                ToastHelper.showToast("xxxApp最新版本失敗!");
            }
            // 下載任務(wù)已經(jīng)完成,清除
            mAppUpgradePersistent.removeDownloadTaskId(context);
            cur.close();
        }
    }

點(diǎn)擊通知欄響應(yīng)廣播

如果還未下載完成,點(diǎn)擊后進(jìn)入系統(tǒng)默認(rèn)的下載界面;下載完成后再點(diǎn)擊,就直接調(diào)用系統(tǒng)安裝界面安裝。

    /**
     * 點(diǎn)擊通知欄下載項(xiàng)目,下載完成前點(diǎn)擊都會(huì)進(jìn)來(lái),下載完成后點(diǎn)擊不會(huì)進(jìn)來(lái)。
     */
    public class NotificationClickReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            long[] completeIds = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
            //正在下載的任務(wù)ID
            long downloadTaskId = mAppUpgradePersistent.getDownloadTaskId(context);
            if (completeIds == null || completeIds.length <= 0) {
                openDownloadsPage(appContext);
                return;
            }

            for (long completeId : completeIds) {
                if (completeId == downloadTaskId) {
                    openDownloadsPage(appContext);
                    break;
                }
            }
        }

        /**
         * Open the Activity which shows a list of all downloads.
         *
         * @param context 上下文
         */
        private void openDownloadsPage(Context context) {
            Intent pageView = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
            pageView.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(pageView);
        }
    }

大家會(huì)發(fā)現(xiàn),在這個(gè)廣播中我們并沒(méi)有看到直接處理下載完成點(diǎn)擊通知欄的代碼。這個(gè)功能我一開(kāi)始也是無(wú)法實(shí)現(xiàn),下載完成后點(diǎn)擊一直都是進(jìn)入系統(tǒng)默認(rèn)的下載頁(yè)面。后面google查閱了一些資料,發(fā)現(xiàn)系統(tǒng)會(huì)調(diào)用View action根據(jù)mimeType去查詢。所以我們要在一開(kāi)始創(chuàng)建DownloadManager.Request時(shí)候調(diào)用Requset.setMimeType方法來(lái)設(shè)置文件類型。

request.setMimeType("application/vnd.android.package-archive");

ok,看到這邊,想必讓你來(lái)實(shí)現(xiàn)檢查版本升級(jí)已然心中有數(shù)。
那接下來(lái)我把遇到的坑,以及是如何埋坑的一一列出來(lái),一定要努力接著往下看哦。。。

坑一

需求:進(jìn)入首頁(yè)后,開(kāi)啟自動(dòng)檢測(cè)升級(jí),檢測(cè)到有升級(jí)的版本就隨時(shí)彈框提示用戶,但此時(shí)用戶可能已經(jīng)在操作APP進(jìn)入其他頁(yè)面,怎么保證彈框可以正常彈出?(APP不會(huì)出現(xiàn)異?;蛘弑罎ⅲ?br> 升級(jí)提示彈框設(shè)置為:

dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);

同時(shí)在AndroidAmanifest.xml加入權(quán)限:

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

我在華為P9上(7.0)上驗(yàn)證測(cè)試沒(méi)有問(wèn)題,找了谷歌Nexus 5(4.4)驗(yàn)證也沒(méi)有問(wèn)題,以為大功告成!
后面我谷歌上找了下關(guān)于WindowManager.LayoutParams.TYPE_SYSTEM_ALERT適配,竟然有很多文章爆出小米會(huì)有問(wèn)題而且解決方案也是麻煩不是很靠譜,鬼知道是不是還有其他品牌機(jī)型會(huì)有適配問(wèn)題,沒(méi)辦法android廠商太多了&系統(tǒng)各種定制!

靠譜的解決方案

項(xiàng)目中為了解決這個(gè)適配問(wèn)題,達(dá)到一勞永逸的目的,我們?cè)O(shè)計(jì)了一個(gè)背景透明的UpgradeActivity,當(dāng)需要彈出升級(jí)提示框時(shí),就啟動(dòng)這個(gè)UpgradeActivity然后再顯示這個(gè)彈框。

public class UpgradeActivity extends BaseActivity {

    boolean isShowDialog = false;

    public static void startInstance(Context context) {
        Intent intent = new Intent(context, UpgradeActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }

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

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus && !isShowDialog) {
            isShowDialog = true;
            //升級(jí)提示框
            AppUpgradeManager.getInstance().foundLatestVersion(this);
        }
    }

  //......
}

有個(gè)需要注意的地方就是彈出框關(guān)閉的時(shí)候要記得同時(shí)銷毀UpgradeActivity?。∵@里沒(méi)有給出代碼,相信你有辦法自己解決(廣播啊、接口listener啊、EventBus啊等等只要能及時(shí)正常關(guān)閉都可以)

坑二

我把下載的apk文件存放在sd卡下 Download目錄里面,絕對(duì)路徑變量命名為uriDownload,啟動(dòng)安裝界面安裝,代碼如下:

        Intent installIntent = new Intent();
        installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        installIntent.setAction(Intent.ACTION_VIEW);
        Uri apkFileUri = Uri.fromFile(apkFile);
        installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        installIntent.setDataAndType(apkFileUri, "application/vnd.android.package-archive");
        try {
            appContext.startActivity(installIntent);
        } catch (ActivityNotFoundException e) {
            Timber.d("installAPKFile exception:%s", e.toString());
        }

一切完成后開(kāi)始在真機(jī)上測(cè)試,7.0以下的真機(jī)測(cè)試都沒(méi)有問(wèn)題。我也以為任務(wù)完成可以交差了,剛好手頭有一部華為P9已經(jīng)升級(jí)到7.0,安裝后測(cè)試下載完成升級(jí)版本然后調(diào)用系統(tǒng)安裝界面安裝直接崩潰了,查看log有這么一句:

android.os.FileUriExposedException: file:///storage/emulated/0/xxx exposed beyond app through Intent.getData()

認(rèn)真一看,異常FileUriExposedException之前從來(lái)沒(méi)碰到過(guò)。趕緊google發(fā)現(xiàn)原來(lái)是Android N之后,
Android 框架執(zhí)行的 StrictMode,API 禁止向您的應(yīng)用外公開(kāi) file://URI。如果一項(xiàng)包含文件 URI 的 Intent 離開(kāi)您的應(yīng)用,應(yīng)用失敗并出現(xiàn) FileUriExposedException異常。
若要在應(yīng)用間共享文件,您應(yīng)發(fā)送一項(xiàng) content://URI,并授予 URI 臨時(shí)訪問(wèn)權(quán)限。進(jìn)行此授權(quán)的最簡(jiǎn)單方式是使用 FileProvider類。 如需有關(guān)權(quán)限和共享文件的更多信息,請(qǐng)參閱共享文件。也就是說(shuō),對(duì)于應(yīng)用間共享文件這塊,Android N中做了強(qiáng)制性要求。

問(wèn)題解決

既然我們知道了是什么問(wèn)題,那就開(kāi)始解決問(wèn)題吧。
1、首先在你的manifest里面增加<provider>元素

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jemlin.app">
    <application
        ...>
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.jemlin.app.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths" />
        </provider>
        ...
    </application>
</manifest>

2、res下創(chuàng)建xml目錄,在xml下創(chuàng)建provider_paths.xml資源文件

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <!--path:需要臨時(shí)授權(quán)訪問(wèn)的路徑(.代表所有路徑) name:就是你給這個(gè)訪問(wèn)路徑起個(gè)名字-->
    <external-path
        name="external_files"
        path="." />
</paths>

3、修改我們剛才調(diào)用安裝界面的代碼,最終如下:

        Intent installIntent = new Intent();
        installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        installIntent.setAction(Intent.ACTION_VIEW);

        Uri apkFileUri;
        // 在24及其以上版本,解決崩潰異常:
        // android.os.FileUriExposedException: file:///storage/emulated/0/xxx exposed beyond app through Intent.getData()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            apkFileUri = FileProvider.getUriForFile(appContext, BuildConfig.APPLICATION_ID + ".provider", apkFile);
        } else {
            apkFileUri = Uri.fromFile(apkFile);
        }
        installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        installIntent.setDataAndType(apkFileUri, "application/vnd.android.package-archive");
        try {
            appContext.startActivity(installIntent);
        } catch (ActivityNotFoundException e) {
            Timber.d("installAPKFile exception:%s", e.toString());
        }

抹一把汗水 囧!Android檢查版本升級(jí)就介紹這些。如您有問(wèn)題請(qǐng)留言;如您覺(jué)得好,就給個(gè)贊吧~~

最后編輯于
?著作權(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)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,741評(píng)論 25 709
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評(píng)論 19 139
  • 公司開(kāi)發(fā)時(shí)候,應(yīng)該最常用的就是APP升級(jí)功能,倒不是說(shuō)的是熱修復(fù)等技術(shù),而是普通的檢測(cè)到服務(wù)器版本比本地手機(jī)版本高...
    青蛙要fly閱讀 4,749評(píng)論 12 86
  • github地址 https://github.com/zhouxu88/APPUpgrade.git 一、簡(jiǎn)介:...
    萬(wàn)戶猴閱讀 6,819評(píng)論 8 98
  • 今天要去拍畢業(yè)照 起的早 吃的也簡(jiǎn)單 要搬磚,搬到機(jī)票錢才能旅游
    007寫了自己的生活閱讀 816評(píng)論 2 3

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