目录

  • 前言
  • 一、实现思路
  • 二、服务端接口
  • 三、UI页面
  • 三、工具类实现
    • 1.检查版本号
    • 2.下载apk
    • 3.安装apk
    • 4.实时更新下载进度
    • 5.完整代码
  • 三、外部使用
  • 总结

前言

版本的升级和更新是一个线上App所必备的功能,App的升级安装包主要通过 应用商店 或者 应用内下载 两种方式获得,大部分app这两种方式都会具备,应用商店只需要上传对应平台审核通过即可,而应用内更新一般是通过以下几种方式:

1.集成第三方库如 appupdateX、bugly 的更新功能
2.手动实现

这里自己从网上找了一些资料,使用 Kotlin 结合自己的想法,完整地实现了一个应用内在线更新的功能,该功能使用 DownloadManager 下载安装包,适配 Android6.0 以上所有版本,现也已经成功应用到自己公司平台上了。如果这不能满足大家的高级需求,也能提供思路和方向,万变不离其宗,清晰的思路永远胜过简单的搬运,下面是具体实现:


一、实现思路

1、通过接口获取版本号和安装包下载地址:完美一点的是应该是解析出安装包里面的版本号
2、比较线上的版本和本地版本,弹出升级弹窗:可在这里设置强制更新,不更新退出
3、下载 APK 安装包:显示进度条,通过 DownloadManager 下载,同时会在手机顶部通知栏显示下载进度,也可通过三方框架(比如 Volley、OkHttp、IntentService )的文件下载功能
4、安装升级包:获取权限和不同版本适配

UI效果:

二、服务端接口

服务端需要提供一个接口,返回下载安装包地址、版本号等信息,Json字符串:

{"result": {"id": 1,"publishTime": "发布时间","name": "app名称","version": "版本号","updateMessage": "更新内容:1.xxx \n 2.xxx","downloadUrl": "下载地址(https://www...com/app名称v4.0.0.apk)"},"success": true,"error": null,
}

对应bean数据类:UpgradeResponse.kt

import androidx.annotation.Keep
/*** version: 4.0.0* publishTime: 当前时间* updateMessage: 1....\n 2....*/
@Keep
data class UpgradeResponse(val id:Int,//更新日期val publishTime: String?,// app名字val name: String,//服务器版本val version: String,//app最新版本地址val downloadUrl: String,//升级信息val updateMessage: String?,
)

三、UI页面

主要添加版本号、发布时间、更新内容、进度条、操作按钮等内容,进度条是 Android 自带的控件,默认隐藏,在点击更新后,隐藏按钮,显示进度条,并动态更新进度。好看的样式都可以自己 DIY,例如找一些火箭发射的专用背景图。

弹窗:dialog_upgrade.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="280dp"android:layout_height="wrap_content"android:background="@drawable/bg_dialog"android:orientation="vertical"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="100dp"android:orientation="vertical"android:background="@drawable/bg_dialog_top"android:paddingLeft="20dp"android:paddingTop="8dp"android:paddingRight="20dp"android:paddingBottom="8dp"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="发现新版本"android:textColor="@color/white"android:textSize="20sp" /><TextViewandroid:id="@+id/tv_version"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="10dp"android:lineSpacingMultiplier="1.2"android:text="版本号:"android:textColor="@color/white"android:textSize="15sp" /><TextViewandroid:id="@+id/tv_date"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="5dp"android:lineSpacingMultiplier="1.2"android:text="发布时间:"android:textColor="@color/white"android:textSize="15sp" /></LinearLayout><TextViewandroid:id="@+id/tv_feature"android:layout_width="match_parent"android:layout_height="wrap_content"android:maxHeight="350dp"android:padding="20dp"android:text="版本特性:"android:textSize="15sp" /><Viewandroid:layout_width="match_parent"android:layout_height="0.6dp"android:background="@color/api_date_text_color_1"/><LinearLayoutandroid:id="@+id/fl_progress"android:layout_width="280dp"android:layout_height="wrap_content"android:gravity="center_vertical"android:orientation="horizontal"android:padding="10dp"android:visibility="gone"tools:visibility="visible"><!--android:max="100"--><ProgressBarandroid:id="@+id/progressBar"style="?android:attr/progressBarStyleHorizontal"android:layout_width="210dp"android:layout_height="wrap_content"android:padding="@dimen/dp_10"android:value="0" /><TextViewandroid:id="@+id/tv_progress"android:layout_width="45dp"android:gravity="center"android:layout_height="wrap_content"android:layout_marginStart="5dp"android:text="0%" /></LinearLayout><LinearLayoutandroid:id="@+id/ll_actions"android:layout_width="280dp"android:layout_height="wrap_content"android:gravity="center_vertical"android:orientation="horizontal"><TextViewandroid:id="@+id/tv_cancel"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:padding="20dp"android:gravity="center"android:text="下次再说"android:textSize="16dp" /><Viewandroid:layout_width="0.6dp"android:layout_height="match_parent"android:background="@color/api_date_text_color_1"/><TextViewandroid:id="@+id/tv_upgrade"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:padding="20dp"android:gravity="center"android:text="立即更新"android:textColor="#42cba6"android:textSize="16dp" /></LinearLayout>
</LinearLayout>

整体白色圆角背景:bg_dialog.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"><corners android:radius="6dp"/><solid android:color="@color/api_white"/>
</shape>

上半部分绿色圆角背景:bg_dialog_top.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"><corners android:topRightRadius="6dp" android:topLeftRadius="6dp"/><solid android:color="#42cba6"/>
</shape>

三、工具类实现

封装一个用于版本更新的工具类 UpgradeUtil.kt ,单例设计,这时候就体现出了 Kotlin 的简便,只用一个 companion object {} 即可,包含操作方法,如果是 Java 引用 Kotlin 方法,方法前面需要加上 @JvmStatic 注解。

1.检查版本号

@JvmStatic //Java使用该方法
fun checkVersion(apkInfo:UpgradeResponse?) :Boolean{if (apkInfo == null) {return false}//完美一点就是先判断包名是否一致,再判断版本号val oldVersion = AppVersionUtils.getVersionCode()//本地版本号//本地版本号 = BaseApplication.getContext().getPackageManager().getPackageInfo(BaseApplication.getContext().getPackageName(), PackageManager.GET_CONFIGURATIONS).versionCodeval version=apkInfo.version.filter { it.isDigit() }.toInt()  //filter过滤器过滤字符,isDigit()只提取数字,防止其他字符混入if ( version > oldVersion) {return true}return false
}

获取本地版本号的工具类(Java):

AppVersionUtils.java

import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import com.auroral.api.BaseApplication;public class AppVersionUtils {private static PackageInfo mPackageInfo;/*** get app version name.** @return version name.*/public static String getVersionName() {getPackageInfo();return mPackageInfo.versionName;}private static void getPackageInfo() {if (mPackageInfo == null) {try {mPackageInfo = BaseApplication.getContext().getPackageManager().getPackageInfo(BaseApplication.getContext().getPackageName(), PackageManager.GET_CONFIGURATIONS);} catch (Exception e) {e.printStackTrace();}}}/*** get app version code.** @return version code.*/public static int getVersionCode() {getPackageInfo();return mPackageInfo.versionCode;}
}

2.下载apk

DownloadManager 是Android系统自带的下载管理工具,可以很好地进行调度下载。 其下载任务会对应唯一个ID, 此id可以用来去查询下载内容的相关信息,获取下载进度。而跳转安装一般是通过 uri 跳转,uri 主要分为以下两类,两种类型都要考虑进去。

  • content://: 系统提供商的media、downloads,第三方的 fileprovider
  • file:// :旧式file类型的uri

在下载之前先判断是否已经下载过,下载过直接跳转,没下载过下载安装后删除下载任务和文件。主要流程:

1、判断是否下载过apk:下载过直接安装
2、DownloadManager配置
3、获取到下载id
4、动态更新下载进度
5、安装apk:两种uri

具体代码中介绍得很详细:

//下载id
private var downloadId=-1L//下载apk
@JvmStatic
fun upgradeApk(context: Context, upgradeInfo: UpgradeResponse,view: View,dialog: Dialog){//设置apk下载地址:本机存储的download文件夹下val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)//找到该路径下的对应名称的apk文件,有可能已经下载过了val file = File(dir, "${upgradeInfo.name}v${upgradeInfo.version}.apk")//开辟线程MainScope().launch {val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager// 1、判断是否下载过apkif (file.exists()) {val authority: String = context.applicationContext.packageName+ ".fileProvider"// "content://" 类型的uri   --将"file://"类型的uri变成"content://"类型的urival uri = FileProvider.getUriForFile(context, authority, file)dialog.dismiss()// 5、安装apk, content://和file://路径都需要installAPK(context,uri,file)}else{// 2、DownloadManager配置val request = DownloadManager.Request(Uri.parse(encodeGB( upgradeInfo.downloadUrl)))  //处理中文下载地址// 设置下载路径和下载的apk名称request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "${upgradeInfo.name}v${upgradeInfo.version}.apk")// 下载时在通知栏内显示下载进度条request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)// 设置MIME类型,即设置下载文件的类型, 如果下载的是android文件, 设置为application/vnd.android.package-archiverequest.setMimeType("application/vnd.android.package-archive")// 3、获取到下载iddownloadId = downloadManager.enqueue(request)// 隐藏按钮显示进度条view.ll_actions.visibility= View.GONEview.tv_progress.text = "0%"view.progressBar.progress = 0view.fl_progress.visibility = View.VISIBLE// 开辟IO线程MainScope().launch(Dispatchers.IO) {// 4、动态更新下载进度val success = checkDownloadProgress(downloadManager,downloadId,view.progressBar,view.tv_progress,file)MainScope().launch {if (success) {// 下载文件"content://"类型的uri ,DownloadManager通过downloadIdval uri = downloadManager.getUriForDownloadedFile(downloadId)// 通过downLoadId查询下载的apk文件转成"file://"类型的urival file= queryDownloadedApk(context, downloadId)dialog.dismiss()// 5、安装apkinstallAPK(context, uri,file)} else {TastyToast.makeText(context, "下载失败",TastyToast.LENGTH_SHORT, TastyToast.WARNING)if (file.exists()) {// 当不需要的时候,清除之前的下载文件,避免浪费用户空间file.delete()}// 删除下载任务和文件downloadManager.remove(downloadId)// 隐藏进度条显示按钮,重新下载view.fl_progress.visibility = View.GONEview.ll_actions.visibility = View.VISIBLE}cancel()}cancel()}}cancel()}
}

中文路径可能导致乱码找不到下载路径,需要转成GB编码

//中文路径转成GB编码
fun encodeGB(string: String): String{//转换中文编码val split = string.split("/".toRegex()).toTypedArray()for (i in 1 until split.size) {try {split[i] = URLEncoder.encode(split[i], "GB2312")} catch (e: UnsupportedEncodingException) {e.printStackTrace()}split[0] = split[0] + "/" + split[i]}split[0] = split[0].replace("\\+".toRegex(), "%20") //处理空格return split[0]
}

3.安装apk

跳转安装 apk 需要适配不同的安卓版本,Android 6.0-7.0 需要老式的 “file://” 的路径,Android 7.0 以上需要 “content://” 的路径

//调用系统安装apk
private fun installAPK(context: Context, apkUri: Uri,apkFile: File?) {val intent = Intent()if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {//安卓7.0版本以上安装intent.action = Intent.ACTION_VIEWintent.flags = Intent.FLAG_ACTIVITY_NEW_TASKintent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)intent.setDataAndType(apkUri, "application/vnd.android.package-archive")} else {//安卓6.0-7.0版本安装intent.action = Intent.ACTION_DEFAULTintent.addCategory(Intent.CATEGORY_DEFAULT)intent.flags = Intent.FLAG_ACTIVITY_NEW_TASKintent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)apkFile?.let {intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive")}}try {context.startActivity(intent)} catch (e: Exception) {e.printStackTrace()}
}

通过 downloadId 获取到 “file://” 的路径

private fun queryDownloadedApk(context: Context, downloadId: Long): File? {var targetApkFile: File? = nullval downloader = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManagerif (downloadId != -1L) {val query = DownloadManager.Query()query.setFilterById(downloadId)query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL)val cur: Cursor? = downloader.query(query)if (cur != null) {if (cur.moveToFirst()) {val uriString: String =cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))if (!TextUtils.isEmpty(uriString)) {targetApkFile = Uri.parse(uriString).path?.let { File(it) }}}cur.close()}}return targetApkFile
}

4.实时更新下载进度

在线程中使用的方法需要带表示 suspend 挂起函数的关键字,通过while循环去读取,监控任务的状态,待状态变成Fail或Success

//检查下载进度
suspend fun checkDownloadProgress(manager: DownloadManager,downloadId: Long,progressBar: ProgressBar,progressText: TextView,file: File
): Boolean {//循环检查,直到状态变成Fail或Successwhile (true) {val q = DownloadManager.Query()q.setFilterById(downloadId)val cursor = manager.query(q)if(cursor.moveToFirst()){val bytes_downloaded =cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))val bytes_total =cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))val dl_progress = (bytes_downloaded * 100 / bytes_total).toInt()progressBar.post {progressBar.progress = dl_progressprogressText.text = "${dl_progress}%"}when (cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))) {DownloadManager.STATUS_SUCCESSFUL -> {return true}DownloadManager.STATUS_RUNNING, DownloadManager.STATUS_PENDING -> {delay(500)}else -> {if (file.exists()) {//当不需要的时候,清除之前的下载文件,避免浪费用户空间file.delete()}manager.remove(downloadId)return false}}}else{if (file.exists()) {//当不需要的时候,清除之前的下载文件,避免浪费用户空间file.delete()}manager.remove(downloadId)return false}}
}

5.完整代码

import android.app.Dialog
import android.app.DownloadManager
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.text.TextUtils
import android.view.View
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.content.FileProvider
import com.auroral.api.utils.AppVersionUtils
import com.sdsmdg.tastytoast.TastyToast
import com.vickn.main.upgrade.bean.UpgradeResponse
import kotlinx.android.synthetic.main.dialog_upgrade.view.*
import kotlinx.coroutines.*
import java.io.File
import java.io.UnsupportedEncodingException
import java.net.URLEncoderclass UpgradeUtil {companion object {private var downloadId=-1L// 上述各类方法// ...}
}

三、外部使用

最后通过网络接口获取到数据后进行版本判断,显示弹窗。接口请求数据和数据监听这里就不列出来了,其次,下载之前必须先对权限进行检查或获取

具体使用:MainActivity中

private val upgradeDialog by lazy{ Dialog(this, R.style.xxx) } //最原生的Dialog, 对应风格样式val isNewVersion = checkVersion(data, this@MainActivity)
if (isNewVersion) {showUpgradeDialog(data)
}//显示版本更新弹窗
private fun showUpgradeDialog(upgradeInfo: UpgradeResponse) {view.tv_version.text = "版本号:${upgradeInfo.version}"upgradeInfo.publishTime?.let {val index = TextUtils.lastIndexOf(it, ':')val date = it.substring(0, index).replace("T", " ")view.tv_date.text = "发布时间:$date"}//当文本被封装到一个类中的某个属性时在传递时会在所有转义字符前加一个"\",例如"\n"变成"\\n"view.tv_feature.text = "版本特性:\n\n${upgradeInfo.updateMessage}".replace("\\n", "\n")view.tv_cancel.setOnClickListener { upgradeDialog.dismiss() }//点击更新view.tv_upgrade.setOnClickListener { //权限申请AndPermission.with(this@MainActivity).runtime().permission(Manifest.permission.WRITE_EXTERNAL_STORAGE).rationale{ context, data, executor ->//显示权限获取的弹窗//.....}.onDenied{TastyToast.makeText(this@MainActivity,"未获得存储权限,无法下载", TastyToast.LENGTH_SHORT, TastyToast.ERROR)}.onGranted{upgradeApk(this@MainActivity, upgradeInfo, view, upgradeDialog)}.start()}upgradeDialog.setContentView(view)upgradeDialog.setCancelable(false)upgradeDialog.show()
}

权限获取使用的是 com.yanzhenjie.permission.AndPermission 的开源第三方包,获取权限的弹窗自己添加。


总结

到这里就全部结束了,不容易呀

【Android】app应用内版本更新升级(DownloadManager下载,适配Android6.0以上所有版本)相关推荐

  1. Android手写签批功能实现(适配Android6.0及以上)

     Android手写签批功能的实现在于三个点,mupdf,偏移量的计算,droidText0.5.jar 实际步骤: 使用muPdf将PDF加载出来 弹出透明的popwindow,popWindow使 ...

  2. Android 系统(98)---Android app 在线更新那点事儿(适配Android6.0、7.0、8.0)

    Android app 在线更新那点事儿(适配Android6.0.7.0.8.0) 一.前言 app在线更新是一个比较常见需求,新版本发布时,用户进入我们的app,就会弹出更新提示框,第一时间更新新 ...

  3. 安卓开发实战之app之版本更新升级(DownloadManager和http下载)完整实现

    前言 本文将讲解app的升级与更新.一般而言用户使用App的时候升级提醒有两种方式获得: 一种是通过应用市场 获取 一种是打开应用之后提醒用户更新升级 而更新操作一般是在用户点击了升级按钮之后开始执行 ...

  4. android app targetsdk从23升级到28

    android app target sdk从23升级到28 一.23升级到24 1.隐式广播 2.权限更改 3.NDK 应用链接至平台库 二.24升级到26 1.后台服务 2.语言区域和国际化 3. ...

  5. Android 使用系统自带的DownloadManager下载apk

    首先扯点别的:清明节回了一趟黄岛,去了学校看了看,也是物是人非了呀.酒也没少喝,前天做了一夜的车早上9点多到上海,然后直接杀奔公司上班,也是没谁了. 完整代码请参考 DownloadManagerAc ...

  6. Android App Dark Theme(暗黑模式)适配指南

    在 2019 年的 Google I/O 和 Apple WWDC 上,新露面的 Android 10 和 iOS 13 都宣布将支持 Dark Theme 也就是我们常说的暗黑模式,并提供相关 AP ...

  7. android调用相机与相册的方法,手把手教你:android调用系统相机、相册功能,适配6.0权限获取以及7.0之后获取URI(兼容多版本)...

    Android中调用系统相机来拍摄照片的代码,以下:html 一.首先设置Uri获取判断以及相机请求Codejava public final int TYPE_TAKE_PHOTO = 1;//Ur ...

  8. Android 通过JNI获取MAC地址(适配Android6.0及以上)

    最近项目中遇到需要在C++层进行加密,然后编译成so.我们知道,MAC地址能够辨别设备的唯一性.所以有个需求就是需要在C++层获取MAC地址,这里我们就需要用到JNI编程了,话不多说,开始看看如何获取 ...

  9. android6.0升级名单,安卓6.0第一批升级名单大全 首批android6.0升级手机名单介绍

    类型:系统工具大小:1.3M语言:中文 评分:5.0 标签: 立即下载 前段时间谷歌正式发布了安卓6.0系统,不少安卓手机用户都想知道自己的手机可不可以升级安卓6.0,近日谷歌也公布了首批安卓6.0升 ...

最新文章

  1. vb中可视对象的操作
  2. 满12万送Mate 30 Pro?华为云“双十一”20+款明星产品齐上线
  3. c++ 预处理命令 #define用法
  4. 为了追到小姐姐,我用 Python 制作了一个机器人
  5. php运算符的关键字,PHP 运算符
  6. Linux进程通信的四种方式——共享内存、信号量、无名管道、消息队列|实验、代码、分析、总结
  7. 为什么NOLOCK查询提示是个不明智的想法
  8. 20种小技巧,玩转Google Colab
  9. 未来集市广告_为什么广告的未来是开放的
  10. Win10搭建python3环境
  11. mysql 搜索_MySQL模糊搜索的几种姿势
  12. 【转】2011年考研备战时间表
  13. Postman的安装
  14. css字体.ttf文件压缩3.1M变8K(原生和Vue中使用)
  15. Pr 音频效果参考:混响
  16. 用dcloud平台的H5+实现消息推送APP端通知栏接收的问题
  17. 群晖服务器名修改,闻上云刷黑群晖后免拆机修改序列号和mac地址
  18. Airspace smoothing
  19. outlook从服务器中恢复已删除项目,Outlook 邮件误删,请问能否恢复?谢谢
  20. openwrt 认证收费_在OpenWrt中安装Wiwiz实现portal认证

热门文章

  1. Easypack: JEECG的容器化编译环境快速构建
  2. go 怎么等待所有的协程完成_优雅地等待子协程执行完毕
  3. 一篇文章带你玩转C语言基础语法5:条件判断 if else 语句与分支 。(千字总结)
  4. 使用浏览器访问服务器shell(ssh方式)
  5. 查看linux版本32还是64位,查看linux系统版本是32位的还是64位的
  6. 摩托车高级驾驶员辅助系统(ADAS)的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告
  7. STM32F4外部中断
  8. 为了不被晒黑,这届年轻人有多拼?| 小红书防晒趋势洞察
  9. STM32debug模式下可以执行,但是不能单步调试和跳转
  10. 2021年质量员-设备方向-通用基础(质量员)考试总结及质量员-设备方向-通用基础(质量员)模拟考试题库