iis服务器助手广告广告
返回顶部
首页 > 资讯 > 精选 >【Android】app应用内版本更新升级(DownloadManager下载,适配Android6.0以上所有版本)
  • 329
分享到

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

androidokhttpkotlinandroidjetpackjava 2023-08-19 12:08:06 329人浏览 八月长安
摘要

目录 前言一、实现思路二、服务端接口三、UI页面三、工具类实现1.检查版本号2.下载apk3.安装apk4.实时更新下载进度5.完整代码 三、外部使用总结 前言 版本的升级和更新是一个线上App所必备的功能,App的升级安


前言

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

集成第三方库如 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@Keepdata 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

<LinearLayout    xmlns: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">    <LinearLayout        android: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">        <TextView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="发现新版本"            android:textColor="@color/white"            android:textSize="20sp" />        <TextView            android: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" />        <TextView            android: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>    <TextView        android: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" />    <View        android:layout_width="match_parent"        android:layout_height="0.6dp"        android:background="@color/api_date_text_color_1"/>    <LinearLayout        android: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">                <ProgressBar            android:id="@+id/progressBar"            style="?android:attr/progressBarStyleHorizontal"            android:layout_width="210dp"            android:layout_height="wrap_content"            android:padding="@dimen/dp_10"            android:value="0" />        <TextView            android:id="@+id/tv_progress"            android:layout_width="45dp"            android:gravity="center"            android:layout_height="wrap_content"            android:layout_marginStart="5dp"            android:text="0%" />    LinearLayout>    <LinearLayout        android:id="@+id/ll_actions"        android:layout_width="280dp"        android:layout_height="wrap_content"        android:gravity="center_vertical"        android:orientation="horizontal">        <TextView            android: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" />        <View            android:layout_width="0.6dp"            android:layout_height="match_parent"            android:background="@color/api_date_text_color_1"/>        <TextView            android: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

<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

<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;        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();            }        }    }        public static int getVersionCode() {        getPackageInfo();        return mPackageInfo.versionCode;    }}

---------------------2023年7月11日 更新--------------------------

BaseApplication类:

public class public class BaseApplication extends Application {    private static Context context;    private static MMKV mmkv;    public static int statusBarHeight = 0;    @Override    public void onCreate() {        super.onCreate();        context = getApplicationContext();        MMKV.initialize(this);        mmkv = MMKV.defaultMMKV();    }    public static Context getContext() {        return context;    }    public static MMKV getMMKV() {        return mmkv;    }} 

2.下载apk

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

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

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

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

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

//下载idprivate var downloadId=-1L//下载apk@JvmStaticfun 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、判断是否下载过apk        if (file.exists()) {            val authority: String = context.applicationContext.packageName+ ".fileProvider"            // "content://" 类型的uri   --将"file://"类型的uri变成"content://"类型的uri            val 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-arcHive            request.setMimeType("application/vnd.android.package-archive")            // 3、获取到下载id            downloadId = downloadManager.enqueue(request)            // 隐藏按钮显示进度条            view.ll_actions.visibility= View.GONE            view.tv_progress.text = "0%"            view.progressBar.progress = 0            view.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通过downloadId                        val uri = downloadManager.getUriForDownloadedFile(downloadId)                        // 通过downLoadId查询下载的apk文件转成"file://"类型的uri                        val file= queryDownloadedApk(context, downloadId)                        dialog.dismiss()                        // 5、安装apk                        installAPK(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.GONE                        view.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://” 的路径

//调用系统安装apkprivate 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_VIEW        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)        intent.setDataAndType(apkUri, "application/vnd.android.package-archive")    } else {    //安卓6.0-7.0版本安装        intent.action = Intent.ACTION_DEFAULT        intent.addCategory(Intent.CATEGORY_DEFAULT)        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK        intent.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? = null    val downloader = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager    if (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或Success    while (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_progress                progressText.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.Dialogimport android.app.DownloadManagerimport android.content.Contextimport android.content.Intentimport android.database.Cursorimport android.net.Uriimport android.os.Buildimport android.os.Environmentimport android.text.TextUtilsimport android.view.Viewimport android.widget.ProgressBarimport android.widget.TextViewimport androidx.core.content.FileProviderimport com.auroral.api.utils.AppVersionUtilsimport com.sdsmdg.tastytoast.TastyToastimport com.vickn.main.upgrade.bean.UpgradeResponseimport kotlinx.android.synthetic.main.dialog_upgrade.view.*import kotlinx.coroutines.*import java.io.Fileimport java.io.UnsupportedEncodingExceptionimport java.net.URLEncoderclass UpgradeUtil {    companion object {    private var downloadId=-1L        // 上述各类方法    // ...}}

三、外部使用

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

具体使用:MainActivity中

private val upgradeDialog by lazy{ Dialog(this, R.style.xxx) } //最原生的Dialog, 对应风格样式private val view by lazy { LayoutInflater.from(this).inflate(R.layout.dialog_upgrade, null) }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 的开源第三方包,获取权限的弹窗自己添加。


总结

到这里就全部结束了,不容易呀😭这算是自己稍微有点技术含量的功能吧,毕竟能拿得出手的不多😂,这必然也会有一些设计上的缺陷和冗余,但无伤大雅。

有问题的也可以在下面评论,我看到都会回复的。

来源地址:https://blog.csdn.net/T01151018/article/details/130561723

--结束END--

本文标题: 【Android】app应用内版本更新升级(DownloadManager下载,适配Android6.0以上所有版本)

本文链接: https://www.lsjlt.com/news/375463.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

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

下载Word文档
猜你喜欢
  • C++ 生态系统中流行库和框架的贡献指南
    作为 c++++ 开发人员,通过遵循以下步骤即可为流行库和框架做出贡献:选择一个项目并熟悉其代码库。在 issue 跟踪器中寻找适合初学者的问题。创建一个新分支,实现修复并添加测试。提交...
    99+
    2024-05-15
    框架 c++ 流行库 git
  • C++ 生态系统中流行库和框架的社区支持情况
    c++++生态系统中流行库和框架的社区支持情况:boost:活跃的社区提供广泛的文档、教程和讨论区,确保持续的维护和更新。qt:庞大的社区提供丰富的文档、示例和论坛,积极参与开发和维护。...
    99+
    2024-05-15
    生态系统 社区支持 c++ overflow 标准库
  • c++中if elseif使用规则
    c++ 中 if-else if 语句的使用规则为:语法:if (条件1) { // 执行代码块 1} else if (条件 2) { // 执行代码块 2}// ...else ...
    99+
    2024-05-15
    c++
  • c++中的继承怎么写
    继承是一种允许类从现有类派生并访问其成员的强大机制。在 c++ 中,继承类型包括:单继承:一个子类从一个基类继承。多继承:一个子类从多个基类继承。层次继承:多个子类从同一个基类继承。多层...
    99+
    2024-05-15
    c++
  • c++中如何使用类和对象掌握目标
    在 c++ 中创建类和对象:使用 class 关键字定义类,包含数据成员和方法。使用对象名称和类名称创建对象。访问权限包括:公有、受保护和私有。数据成员是类的变量,每个对象拥有自己的副本...
    99+
    2024-05-15
    c++
  • c++中优先级是什么意思
    c++ 中的优先级规则:优先级高的操作符先执行,相同优先级的从左到右执行,括号可改变执行顺序。操作符优先级表包含从最高到最低的优先级列表,其中赋值运算符具有最低优先级。通过了解优先级,可...
    99+
    2024-05-15
    c++
  • c++中a+是什么意思
    c++ 中的 a+ 运算符表示自增运算符,用于将变量递增 1 并将结果存储在同一变量中。语法为 a++,用法包括循环和计数器。它可与后置递增运算符 ++a 交换使用,后者在表达式求值后递...
    99+
    2024-05-15
    c++
  • c++中a.b什么意思
    c++kquote>“a.b”表示对象“a”的成员“b”,用于访问对象成员,可用“对象名.成员名”的语法。它还可以用于访问嵌套成员,如“对象名.嵌套成员名.成员名”的语法。 c++...
    99+
    2024-05-15
    c++
  • C++ 并发编程库的优缺点
    c++++ 提供了多种并发编程库,满足不同场景下的需求。线程库 (std::thread) 易于使用但开销大;异步库 (std::async) 可异步执行任务,但 api 复杂;协程库 ...
    99+
    2024-05-15
    c++ 并发编程
  • 如何在 Golang 中备份数据库?
    在 golang 中备份数据库对于保护数据至关重要。可以使用标准库中的 database/sql 包,或第三方包如 github.com/go-sql-driver/mysql。具体步骤...
    99+
    2024-05-15
    golang 数据库备份 mysql git 标准库
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作