文章详情

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

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2023-08-19 12:48

关注

版本的升级和更新是一个线上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?,)

主要添加版本号、发布时间、更新内容、进度条、操作按钮等内容,进度条是 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

阅读原文内容投诉

免责声明:

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

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

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯