我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Android实现App内自动升级,适配了安卓7、8及以上版本

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Android实现App内自动升级,适配了安卓7、8及以上版本

        应用发布后,要实现灰度升级控制,如果只依赖各家应用市场是不够的,还需要自己在应用中控制升级逻辑。并且每家应用市场上新审核也是一件很麻烦的事情,尤其像至简网格这样的应用,甚至没在应用市场上架,更不可能依赖它们了。所以必须要在应用中实现自动升级功能。

        网上有很多介绍,他们摸索的结果对我有很大帮助。可能是因为版本关系,或者关注点不同,照着做,会有很多过时的或错误的地方,所以我将摸索过程记录在此,防止忘记。

        下面几个图是在华为荣耀V9(安卓7.0、SDK 24)中的界面:

图1、提醒有可升级的版本

图2、下载版本

图3、安卓7.0的安全检测界面 

     大致步骤如下:

  1. AndroidManifest及res设置;
  2. 申请外部存储读写权限;
  3. 申请安装应用;
  4. 向服务端查询是否有可升级版本,下载版本,执行安装;

       安卓各个版本差异较大,我的测试日期为(2023.5.29),测试环境为小米8(安卓10、SDK29)、华为荣耀v9(安卓7.0、SDK 24)两种。因为不考虑兼容安卓7之前的版本,所以代码中也无相关实现。

一、AndroidManifest及res设置

1、AndroidManifest设置

增加以下权限:

                            ......                                        

有以下几点需要注意:

  • application中需要增加属性android:requestLegacyExternalStorage="true";
  • provider属性android:authorities="${applicationId}.fileprovider",这个名称可以自己定,但是在执行安装时必须保持一致,后面会再次提到;
     
  • provider中meta-data->android:resource="@xml/autoupdate"名称可以自己定,但是需确保在res/xml/下有同名的xml文件,Android7.0及以上版本需要通过FileProvider方式进行安装,文件内容见下一节;

    2、res中的准备

  • 在res中新建一个xml目录,创建autoupdate.xml,内容如下,注意其中的注释;
        
  • 下载安装界面定义

        在res/layout中增加download_dlg.xml,用以显示下载进度及安装中碰到的问题,怎样显示请看后面的Updater类实现。

        

      

二、申请外部存储读写权限与安装权限

        安卓5.0之后,申请权限的操作变化很大,使用起来比老版本好一些,同一个逻辑不会被打散到多个地方实现。以下实现在MainActivity.onCreate中调用,没有考虑兼容老版本。里面有个updater.checkVersion调用,后面会讲到,用来查询服务端是否有新版本,可以根据自己的需要做不同的实现。因为只有在具备外部存储读写权限后才可以执行升级操作,所以申请成功后,才会检查是否有可升级版本。如果没有新版本,是不会出现申请安装应用权限的界面,否则系统的提示会吓退一部分用户。

         申请安装权限的实现与申请外部存储读写权限不同,在安卓8.0(SDK26)后有一次大变动。在后面的Updater类中,如果是8.0之前的版本则直接安装,否则要申请权限。

        //申请必要的权限        Updater updater = new Updater(this);                ActivityResultLauncher installApkLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),        (result) -> {//安装申请确认完毕后的回调            if (result.getResultCode() == Activity.RESULT_OK) {                updater.showDownloadDialog();            }        });                registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(),            result -> {//权限申请执行完毕后的回调                String permissions = "";                boolean allPassed = true;                for (Map.Entry p : result.entrySet()) {                    permissions += p.getKey() + ':' + p.getValue() + '\n';                    if(!p.getValue()) {                        allPassed = false;                    }                }                LOG.debug("STORAGE_PERMISSION grantResults:\n{}", permissions);                if(allPassed) { //有了外部存储读写权限之后再判断是否有升级版本                    updater.checkVersion(installApkLauncher);                }            }        ).launch(new String[] {            Manifest.permission.WRITE_EXTERNAL_STORAGE,            Manifest.permission.READ_EXTERNAL_STORAGE        });

      

三、升级安装

        检查版本、下载、安装,都在Updater类中实现,在申请外部存储读写权限、申请安装权限时,会调用到Updater中的函数。

        代码中出现的cn.net.zhijian包下的类都是我的公共类,看的时候可以忽略,根据函数名称应该能大致猜出它的功能。

        注意其中的String authority = BuildConfig.APPLICATION_ID + ".fileprovider";前面提到过,必须与provider定义中保持一致。否则会提示Couldn't find meta-data for provider with authority...错误。

     installApk(File apkFile, String digest, AbsHttpCallback.IDownloadProgress progress)

  1. apkFile:已下载的安装文件,我指定的路径是context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)+"/app.apk",似乎autoupdate.xml中的设置在此并未起什么作用;
  2. digest:从我的服务器上查到的文件md5值,安装前比较校验码,不同则拒绝安装;
  3. progress:用以提示下载进度、安装错误信息等;
  showUpdateDialog(ActivityResultLauncher installPermApply)

      installPermApply是在MainActivity.onCreate中初始化安装权限申请加载器时传递进来的。安卓8.0及以上版本才会调用它,其他情况则直接显示下载安装界面。

import android.app.Activity;import android.content.DialogInterface;import android.content.Intent;import android.net.Uri;import android.os.Build;import android.os.Environment;import android.provider.Settings;import android.view.LayoutInflater;import android.view.View;import android.widget.ProgressBar;import android.widget.TextView;import androidx.activity.result.ActivityResultLauncher;import androidx.appcompat.app.AlertDialog;import androidx.core.content.FileProvider;import org.slf4j.Logger;import java.io.File;import java.util.HashMap;import java.util.List;import java.util.Map;import cn.net.zhijian.mesh.client.abs.AbsHttpCallback;import cn.net.zhijian.mesh.client.abs.IConst;import cn.net.zhijian.mesh.client.abs.IThreadPool;import cn.net.zhijian.mesh.client.bean.Company;import cn.net.zhijian.mesh.client.bean.RequestOptions;import cn.net.zhijian.mesh.client.util.HttpClient;import cn.net.zhijian.meshclient.BuildConfig;import cn.net.zhijian.meshclient.R;import cn.net.zhijian.util.FileUtil;import cn.net.zhijian.util.HttpUtil;import cn.net.zhijian.util.LogUtil;import cn.net.zhijian.util.StringUtil;import cn.net.zhijian.util.UrlPathInfo;import cn.net.zhijian.util.ValParser;class Updater {    private static final Logger LOG = LogUtil.getInstance();    private final Activity context;    private String verFromSrv; //服务端返回的应用版本号    private String cdnUrl; //服务端返回的CDN头部地址,后面加上/app_id/version/app.apk    private String digest; //服务端返回的应用apk校验码    private int size; //服务端返回的应用apk大小    private List features; //服务端返回的新版本的特性列表    public Updater(Activity context) {        this.context = context;    }    private void showUpdateDialog(ActivityResultLauncher installPermApply) {        AlertDialog.Builder builder = new AlertDialog.Builder(context);        builder.setTitle(R.string.there_is_new_ver);        builder.setIcon(R.drawable.download);        StringBuilder sb = new StringBuilder();        sb.append(context.getString(R.string.ver_no)).append(this.verFromSrv).append('\n');        for(String f : features) {            sb.append(f).append('\n');        }        builder.setMessage(sb.toString());        builder.setPositiveButton(R.string.update_rightnow, (DialogInterface dialog, int which) -> {            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {                boolean haveInstallPermission = context.getPackageManager().canRequestPackageInstalls();                if (!haveInstallPermission) { //如果已经有权限,不必再申请                    Uri packageURI = Uri.parse("package:" + BuildConfig.APPLICATION_ID);                    Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageURI);                    installPermApply.launch(intent); //权限申请通过后执行showDownloadDialog                    return;                }            }            showDownloadDialog();// 版本<26(Android 8)或已申请了权限,则直接显示下载安装        });        builder.setNegativeButton(R.string.do_it_later, (DialogInterface dialog, int which) -> {            dialog.dismiss();        });        builder.create().show();    }        public void showDownloadDialog() {        AlertDialog.Builder builder = new AlertDialog.Builder(context);        builder.setTitle(R.string.update_apk);        LayoutInflater inflater = LayoutInflater.from(context);        View v = inflater.inflate(R.layout.download_dlg, null);        ProgressBar progressBar = (ProgressBar) v.findViewById(R.id.progress);        TextView txtMsg = v.findViewById(R.id.txtMsg);        builder.setView(v);        builder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {            //canceled = true;        });        //用于显示当前的进度,请参照download_dlg.xml中的UI定义        AbsHttpCallback.IDownloadProgress progress = new AbsHttpCallback.IDownloadProgress() {            String header = "";            @Override            public void progress(int curSize) {                int percent = (int) (((float) curSize / size) * 100);                context.runOnUiThread(() -> {                    txtMsg.setText(header + percent + "%");                    progressBar.setProgress(percent);                });            }            @Override            public void message(String msg) {                this.header = msg;                context.runOnUiThread(() -> {                    txtMsg.setText(header);                });            }        };        builder.create().show();        String url = cdnUrl;        if(!url.endsWith("/")) {            url += '/';        }        url += BuildConfig.APPLICATION_ID + '/' + this.verFromSrv + "/app.apk";        String saveAs = FileUtil.addPath(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app.apk");        File f = new File(saveAs);        if(f.exists()) { //如果文件存在,并且校验码相同,则不必再次下载            String localDigest = FileUtil.digest(f);            if(digest.equals(localDigest)) {                LOG.info("Reinstall apk {}, size:{}", saveAs, size);                context.runOnUiThread(() -> {                    progress.progress(size);                    installAPK(new File(saveAs), digest, progress);                });                return;            }        }        progress.message(context.getString(R.string.downloading));        HttpClient.download(url, saveAs, progress).whenCompleteAsync((hr, e) -> {            if(e != null) {                LOG.error("Fail to download {}", cdnUrl, e);                return;            }            if(hr.code != RetCode.OK || hr.data == null || hr.data.size() == 0) {                LOG.error("Fail to download {}, result:{}", cdnUrl, hr.brief());                return;            }            int appSize = ValParser.getAsInt(hr.data, "size");            if(appSize != size) {                LOG.error("Fail to download {}, invalid size({}!={}}", cdnUrl, size, appSize);                return;            }            LOG.info("Reinstall apk {}, size:{}", ValParser.getAsStr(hr.data, "saveAs"), size);            context.runOnUiThread(() -> {                installAPK(new File(saveAs), digest, progress);            });        });    }        private void installAPK(File apkFile, String digest, AbsHttpCallback.IDownloadProgress progress) {        progress.message(context.getString(R.string.installing));        try {            if (!apkFile.exists()) {                LOG.error("Update apk file `{}` not exists", apkFile);                progress.message(context.getString(R.string.apk_not_exists));                return;            }            String localDigest = FileUtil.digest(apkFile);            if(!localDigest.equals(digest)) {                LOG.error("Invalid apk file `{}` digest({}!={})", apkFile, localDigest, digest);                progress.message(context.getString(R.string.wrong_digest));                return;            }            Intent intent = new Intent(Intent.ACTION_VIEW);            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//安装完成后打开新版本            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 给目标应用一个临时授权            //Build.VERSION.SDK_INT >= 24,使用FileProvider兼容安装apk            //packageName也可以通过context.getApplicationContext().getPackageName()获取            String authority = BuildConfig.APPLICATION_ID + ".fileprovider";            Uri apkUri = FileProvider.getUriForFile(context, authority, apkFile);            intent.setDataAndType(apkUri, "application/vnd.android.package-archive");            context.startActivity(intent);            //安装完之后会提示”完成” “打开”。            android.os.Process.killProcess(android.os.Process.myPid());        } catch (Exception e) {            LOG.error("Fail to install apk {}", apkFile, e);            progress.message(context.getString(R.string.fail_to_install));        }    }    public void checkVersion(ActivityResultLauncher installPermApply) {        Company company = RequestOptions.getCompany(Company.PERSONAL_COMPANY_ID);        int localVer = StringUtil.verToInt(IConst.VERSION);        UrlPathInfo url = new UrlPathInfo("/checkAppVer")                .appendPara("service", BuildConfig.APPLICATION_ID, false)                .appendPara("ver", localVer, false)                .appendPara("evm", "Android_" + Build.VERSION.SDK_INT, false);        Map req = new HashMap<>();        req.put("url", url.toString());        req.put("method", HttpUtil.METHOD_GET);        req.put("private", false);        RequestOptions.parse(company, req, IConst.SERVICE_APPSTORE).thenComposeAsync(opts -> {            return HttpClient.get(opts.url.node, opts.url.url(), opts.headers);        }, IThreadPool.Pool).whenCompleteAsync((hr, e) -> {            if(e != null) {                LOG.error("Fail to get service info from cloud", e);                return;            }            if(hr.code == RetCode.NOT_EXISTS) {                LOG.info("No update version for {}", url);                return;            }            if(hr.code != RetCode.OK || hr.data == null || hr.data.size() == 0) {                LOG.error("Fail to get service info from cloud, result:{}", hr.brief());                return;            }            LOG.debug("checkVersion:{}", hr.data);            int serverVer = ValParser.getAsInt(hr.data, "ver");            if(localVer < serverVer) {                this.verFromSrv = StringUtil.intToVer(serverVer);                this.cdnUrl = ValParser.getAsStr(hr.data, "url");                this.digest = ValParser.getAsStr(hr.data, "digest");                this.size = ValParser.getAsInt(hr.data, "size");                this.features = ValParser.getAsStrList(hr.data, "features");                context.runOnUiThread(() -> {                    showUpdateDialog(installPermApply);                });            }        }, IThreadPool.Pool);    }}

希望以上内容对你有点帮助,如果有什么问题,欢迎留言评论,我尽量完善它。

此文只在CSDN上编辑修改过,有网站转载了老版本的,里面存在错误,请注意。

来源地址:https://blog.csdn.net/flyinmind/article/details/130925867

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Android实现App内自动升级,适配了安卓7、8及以上版本

下载Word文档到电脑,方便收藏和打印~

下载Word文档

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录