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

您所在的位置:网站首页 购买E卡管控升级 Android实现App内自动升级,适配了安卓7、8及以上版本

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

2024-07-14 15:45:41| 来源: 网络整理| 查看: 265

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

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

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

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

图2、下载版本

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

     大致步骤如下:

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

       安卓各个版本差异较大,我的测试日期为(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); /* * 申请安装应用的权限。 * registerForActivityResult必须在onCreate中调用, * 否则会报错:LifecycleOwners must call register before they are STARTED. */ ActivityResultLauncher installApkLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), (result) -> {//安装申请确认完毕后的回调 if (result.getResultCode() == Activity.RESULT_OK) { updater.showDownloadDialog(); } }); /* * 申请外部存储卡读写权限。 * 调用Environment.getExternalStoragePublicDirectory等函数,必须具备外部存储读写权限, * 除了在manifest中要声明权限,同时在application中设置android:requestLegacyExternalStorage="true" * 并且,还需要在代码中动态申请。 * 申请成功后才能确定应用升级可以执行下去,所以才会查询新版本。 */ 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)

apkFile:已下载的安装文件,我指定的路径是context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)+"/app.apk",似乎autoupdate.xml中的设置在此并未起什么作用;digest:从我的服务器上查到的文件md5值,安装前比较校验码,不同则拒绝安装;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();// 版本 { 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); }); }); } /** * 安装apk * @param apkFile apk文件完整路径 * @param digest 校验码 * @param 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上编辑修改过,有网站转载了老版本的,里面存在错误,请注意。



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭