Android检查版本升级应该怎么做?

加菲猫.jpeg

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

背景

今年年初项目终于上线,到目前为止发布了4个版本。经历了3.8节,整体表现稳定。在第三个版本我们加入了版本检查升级,发布第四版本,用户就直接体验到了这个功能。

必要性

我们开发一个APP,应该是发布第一个版本之后,后续不断的更新迭代。现在大部分APP都是发布到各大应用市场市场,然后用户去搜索我们的应用并下载安装。如果有新的版本,你不可能让每个用户去应用市场重新下载新的版本,又或者用户没注意应用市场的更新提醒导致没有安装最新版本等,所以我们有必要让我们的应用自己检查是否有新的版本。

升级流程图

在APP首页自动触发向服务端请求最新的版本信息,如果服务端返回的版本信息中versionCode与当前版本不一致,就弹出升级提示框让用户选择。流程图如下:

检查更新流程图.png

“立即更新”:直接启动下载
“稍后提醒”:什么都不处理
“忽略该版本”:当前版本不再提醒,如有更新版本还是要提醒。例如:用户版本是1.0,当前检查到的版本是2.0,用户选择了“忽略该版本”,则2.0的版本不再提示,到下个版本时,仍然要提示版本更新。

实现

首先看下app module的build.gradle

每次发布一个新版本时,一般都会修改versionCode以及versionName

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

其中versionCode是整型,这里定义从1开始,每次迭代一个版本就加1;versionName是字符类型,从1.0.0开始,每次更新可以改为1.0.1、1.1.0、2.0.0等等。

接着判断是否有升级版本

通过和后端定好的http api从服务器端请求最新版本的versionCode,然后与当前版本的versionCode比较,如果服务端返回版本信息的versionCode大于当前版本的versionCode,就说明有新的版本需要更新。

1、http api检查版本更新接口response数据格式(仅供参考):

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

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

2、定义VersionInfo模型,用于GSon解析服务端返回的数据:

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

3、如果有升级版本,随时弹窗提示用户。没有升级版本,就不用提示。

忽略更新

我的做法是把用户忽略更新的版本号versionCode存储到sharePreference中,每次发现有升级版本时,在给用户提示之前,先取忽略版本号versionCode与最新版本号versionCode比较是否一样,如果一样就什么都不做,检查更新结束;如果不一样,还是照样给用户提示。

//取sp中保存的versionCode
int versionCode = mAppUpgradePersistent.getIgnoreUpgradeVersionCode(appContext);
if (versionCode == latestVersion.getVersionCode()) {
  //用户之前已经选择"忽略该版本",不更新这个版本。
  Timber.d("[AppUpgradeManager] ignore upgrade version====");
  return;
}

稍后提醒

最简单,什么都不做。

立即更新

1、项目中我们直接使用系统提供的DownloadManager服务,同时注册两个广播:
下载完成广播DownloadManager.ACTION_DOWNLOAD_COMPLETE以及
点击下载通知栏广播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、如果可以的话,在用户选择立即更新之后,您的应用应该判断当前的网络环境,如果是非wifi环境应该弹窗提示用户类似“您当前使用的不是wifi,更新会产生一些网络流量,是否继续下载?”
代码片段如下:

            // 非wifi网络下,再次提示用户是否继续
            MaterialDialog.Builder builder = new MaterialDialog.Builder(activity);
            final MaterialDialog dialog = builder.title("流量提醒")
                    .theme(Theme.LIGHT)
                    .titleGravity(GravityEnum.CENTER)
                    .content("您当前使用的不是wifi,更新会产生一些网络流量,是否继续下载?")
                    .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、真正调用DownloadManager下载前,我们可以判断下本地是否已经过了最新版本,有就直接启动安装界面安装;下载的不是最新版本,就直接删除。
代码片段如下:

        //先检查本地是否已经有需要升级版本的安装包,如有就不需要再下载
        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安装包,与服务器上apk安装包的版本号是否一致
                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、创建下载Reuqst,开始下载。
代码片段如下:

        Request task = new Request(Uri.parse(latestVersion.getDownloadUrl()));
        //定制Notification的样式
        String title = "应用名称:" + latestVersion.getVersion();
        task.setTitle(title);
        task.setDescription(latestVersion.getVersionDesc());
       //如果我们希望下载的文件可以被系统的Downloads应用扫描到并管理,我们需要调用Request对象的setVisibleInDownloadsUi方法,传递参数true
        task.setVisibleInDownloadsUi(true);
        //设置是否允许手机在漫游状态下下载
        task.setAllowedOverRoaming(false);
        //限定在WiFi下进行下载
        task.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE);
        task.setMimeType("application/vnd.android.package-archive");
        // 在通知栏通知下载中和下载完成
        // 下载完成后该Notification才会被显示
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) {
            // 3.0(11)以后才有该方法
            //在下载过程中通知栏会一直显示该下载的Notification,在下载完成后该Notification会继续显示,直到用户点击该Notification或者消除该Notification
            task.allowScanningByMediaScanner();
            task.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        }
        // 可能无法创建Download文件夹,如无sdcard情况,系统会默认将路径设置为/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)方法中已经注册了监听下载完成广播,一旦我们知道下载完成的时机,就可以调用系统安装界面安装我们的APK啦~
切记,不可在广播的onReceive中做耗时操作,时间不能超过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最新版本失败!");
            }
            // 下载任务已经完成,清除
            mAppUpgradePersistent.removeDownloadTaskId(context);
            cur.close();
        }
    }

点击通知栏响应广播

如果还未下载完成,点击后进入系统默认的下载界面;下载完成后再点击,就直接调用系统安装界面安装。

    /**
     * 点击通知栏下载项目,下载完成前点击都会进来,下载完成后点击不会进来。
     */
    public class NotificationClickReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            long[] completeIds = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
            //正在下载的任务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);
        }
    }

大家会发现,在这个广播中我们并没有看到直接处理下载完成点击通知栏的代码。这个功能我一开始也是无法实现,下载完成后点击一直都是进入系统默认的下载页面。后面google查阅了一些资料,发现系统会调用View action根据mimeType去查询。所以我们要在一开始创建DownloadManager.Request时候调用Requset.setMimeType方法来设置文件类型。

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

ok,看到这边,想必让你来实现检查版本升级已然心中有数。
那接下来我把遇到的坑,以及是如何埋坑的一一列出来,一定要努力接着往下看哦。。。

坑一

需求:进入首页后,开启自动检测升级,检测到有升级的版本就随时弹框提示用户,但此时用户可能已经在操作APP进入其他页面,怎么保证弹框可以正常弹出?(APP不会出现异常或者崩溃)
升级提示弹框设置为:

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

同时在AndroidAmanifest.xml加入权限:

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

我在华为P9上(7.0)上验证测试没有问题,找了谷歌Nexus 5(4.4)验证也没有问题,以为大功告成!
后面我谷歌上找了下关于WindowManager.LayoutParams.TYPE_SYSTEM_ALERT适配,竟然有很多文章爆出小米会有问题而且解决方案也是麻烦不是很靠谱,鬼知道是不是还有其他品牌机型会有适配问题,没办法android厂商太多了&系统各种定制!

靠谱的解决方案

项目中为了解决这个适配问题,达到一劳永逸的目的,我们设计了一个背景透明的UpgradeActivity,当需要弹出升级提示框时,就启动这个UpgradeActivity然后再显示这个弹框。

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;
            //升级提示框
            AppUpgradeManager.getInstance().foundLatestVersion(this);
        }
    }

  //......
}

有个需要注意的地方就是弹出框关闭的时候要记得同时销毁UpgradeActivity!!这里没有给出代码,相信你有办法自己解决(广播啊、接口listener啊、EventBus啊等等只要能及时正常关闭都可以)

坑二

我把下载的apk文件存放在sd卡下 Download目录里面,绝对路径变量命名为uriDownload,启动安装界面安装,代码如下:

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

一切完成后开始在真机上测试,7.0以下的真机测试都没有问题。我也以为任务完成可以交差了,刚好手头有一部华为P9已经升级到7.0,安装后测试下载完成升级版本然后调用系统安装界面安装直接崩溃了,查看log有这么一句:

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

认真一看,异常FileUriExposedException之前从来没碰到过。赶紧google发现原来是Android N之后,
Android 框架执行的 StrictMode,API 禁止向您的应用外公开 file://URI。如果一项包含文件 URI 的 Intent 离开您的应用,应用失败并出现 FileUriExposedException异常。
若要在应用间共享文件,您应发送一项 content://URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider类。 如需有关权限和共享文件的更多信息,请参阅共享文件。也就是说,对于应用间共享文件这块,Android 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下创建xml目录,在xml下创建provider_paths.xml资源文件

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <!--path:需要临时授权访问的路径(.代表所有路径) name:就是你给这个访问路径起个名字-->
    <external-path
        name="external_files"
        path="." />
</paths>

3、修改我们刚才调用安装界面的代码,最终如下:

        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检查版本升级就介绍这些。如您有问题请留言;如您觉得好,就给个赞吧~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容