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

作者: 磐龍 | 来源:发表于2017-03-14 11:44 被阅读3556次
    加菲猫.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检查版本升级就介绍这些。如您有问题请留言;如您觉得好,就给个赞吧~~

    相关文章

      网友评论

      • 取不动名字了:赞赞赞,写的很详细
      • 德才兄:坑一的解决方案没懂,启动界面调用升级接口没问题,用户在使用App其他界面怎么告知用户升级?
        磐龍:检测到有升级版本,就马上弹出界面,不管用户此时在App的哪个界面,都可以正常弹出升级提示,界面是UpgradeActivity,你搜索下。
      • b4edba7f37e0:这个坑总结的好
      • 阿瑞921:良心作品,学到了,希望作者以后能继续写出高质量的文章
        阿瑞921: @Jemlin 好东西,就应该支持
        磐龍:谢谢支持:smile:
      • 用一辈子等待一个幸福:public static final String API_DOMAIN_DEBUG = "配置你自己的服务器基地址";
        public static final String API_DOMAIN_RELEASE = "配置你自己的服务器基地址";
        这两句话使用来干什么的?是接口吗?
        磐龍:@大少爷丶 host 地址,后台服务会部署到一台服务器上,他们会给你一个host地址!比如http://abc.com
        用一辈子等待一个幸福:@Jemlin 不好意思,不太懂,能给分析下吗?谢谢
        磐龍:@大少爷丶 是的,线上和线下的服务后台基地址
      • snow2015:大神,你试过华为 P9 plus 7.0吗?好像下载不了。。。
        可否给一个你们线上的APP的下载地址,我想试一下。。。
        snow2015:@Jemlin 下载一直是挂起状态,没有任何提示。。。
        你们前一个版本的APP下载链接可以可给我吗?
        磐龍:你自己的APP更新下载不了,是提示什么问题?
        磐龍:@snow2015 你到豌豆荚搜索 好慷在家。。我这里用华为P9 7.0是可以的。
      • 觅道中人3:楼主你好 ,我遇到的问题是一部三星6.0手机,无法正常打开下载后的apk(PackageInstallActivity都不能打开 报错no activity found),其他手机包括其他三星都可以正常打开,按你“坑二”处理了,可以看运行界面貌似能打开安装activity ,但出现解析错误“解析软件包时出现问题”
        觅道中人3:@Jemlin 你好!!!!我找到问题了,是我的错!!file对象获取方式有问题, 现在解决了,非常感谢你!今天我觉得很幸运,我今天早上刚发现这台三星不能自动安装的问题,早上一直在找解决方法,stackoverflow上也找不到类似情况,中午吃饭还在惦记这事儿,下午就看到优雅程序员转发的你的博客,我看了标题就感觉这真的太巧了!!!!
        觅道中人3:@Jemlin 嗯,验证过了 ,在文件管理器中手动点击正常打开安装
        磐龍:@觅道中人3 先确定下载的apk 是不是真的有问题,验证就是你手动安装这个包,试试看
      • 因帅被判刑:大佬 给个demo吧
        磐龍:demo演示地址:https://github.com/pzl237/UpgradeDemo
      • cjcj125125:很不错刚好用上,但是碰到些问题,能否给分demo参考下呢??万分感谢
        磐龍:demo演示地址:https://github.com/pzl237/UpgradeDemo
        cjcj125125:@Jemlin 好的,谢谢
        磐龍:@cjcj125125 没有单独写demo了,有什么问题可以留言沟通:joy:
      • 胜_弟:请问 可以分享更新Demo么 方便的话请发一份给我额 298103331@qq.com
        磐龍:@胜_弟 demo演示地址:https://github.com/pzl237/UpgradeDemo
        胜_弟:@Jemlin 好滴,期待额
        磐龍:@胜_弟 遇到什么问题可以留言沟通,后面有时间再整理个demo。
      • azhunchen:赞,沙发

      本文标题:Android检查版本升级应该怎么做?

      本文链接:https://www.haomeiwen.com/subject/ocrngttx.html