Android自动更新实现(Kotlin语言)

本文详细介绍了如何在Android应用中使用Java编写一个自动更新功能,包括网络请求APK文件、校验更新、下载管理以及安装过程,涉及HTTPS安全处理和Android权限管理。
摘要由CSDN通过智能技术生成

Java语言的实现可参考这位大佬的文章->传送门

一、新建一个AutoUpdater类

AutoUpdater.kt代码如下

package com.zwb.a2mesapp

import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Message
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.FileProvider
import java.io.BufferedReader
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.MalformedURLException
import java.net.URL
import java.security.SecureRandom
import java.util.regex.Pattern
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory

public class AutoUpdater(context:Context) {
    // 下载安装包的网络路径
    private var apkUrl:String  = "http://192.168.199.178:8003/"
    protected var checkUrl:String  = apkUrl + "output-metadata.json"



    // 下载线程
    private var downLoadThread:Thread?=null
    private var progress:Int=0 // 当前进度
    // 应用程序Context
    val mContext: Context=context
    // 保存APK的文件名
    private final val saveFileName:String  = "my.apk"
    private val apkFile:File = File(mContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), saveFileName);
    // 是否是最新的应用,默认为false
    private var isNew:Boolean  = false
    private var intercept:Boolean  = false
    // 进度条与通知UI刷新的handler和msg常量
    private var mProgress: ProgressBar?=null
    private var txtStatus: TextView?=null

    private final val DOWN_UPDATE:Int  = 1
    private final val DOWN_OVER:Int  = 2;
    private final val SHOWDOWN:Int  = 3;


    public fun ShowUpdateDialog() {
        val builder:AlertDialog.Builder=AlertDialog.Builder(mContext)
        .setCancelable(false)
        .setTitle("软件版本更新")
        .setMessage("有最新的软件包,请下载并安装!")
        .setPositiveButton("立即下载",{dialog, which ->
            //回调触发
                Toast.makeText(this.mContext, "下载", Toast.LENGTH_SHORT).show()
                ShowDownloadDialog();
        })
        .setNegativeButton("以后再说",{dialog,which->
                dialog.dismiss()
        })

        builder.create().show();
    }

    private fun ShowDownloadDialog() {
        val dialog:AlertDialog.Builder  = AlertDialog.Builder(this.mContext)

        dialog.setCancelable(false)
        dialog.setTitle("软件版本更新")
        var inflater: LayoutInflater = LayoutInflater.from(this.mContext)
        var v: View = inflater.inflate(R.layout.progress, null)
        mProgress = v.findViewById(R.id.progress)
        txtStatus = v.findViewById(R.id.txtStatus)
        dialog.setView(v)
        dialog.setPositiveButton("取消"){dialog,which->
                this.intercept=true
        }
        dialog.show();
        DownloadApk();
    }

    /**
     * 检查是否更新的内容
     */
    open fun CheckUpdate() {
        Thread(Runnable {
            var localVersion = "1"
            try {
                localVersion =
                    mContext.packageManager.getPackageInfo(mContext.packageName, 0).versionName
            } catch (e: PackageManager.NameNotFoundException) {
                e.printStackTrace()
                Log.e("ChceckUpdate_Error",e.message.toString())
            }
            var versionName = "1"
            var outputFile = ""
            val config = doGet(checkUrl)
            if (config != null && config.length > 0) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    var m = Pattern.compile("\"outputFile\":\\s*\"(?<m>[^\"]*?)\"").matcher(config)
                    if (m.find()) {
                        outputFile = m.group("m")
                    }
                    m = Pattern.compile("\"versionName\":\\s*\"(?<m>[^\"]*?)\"").matcher(config)
                    if (m.find()) {
                        val v = m.group("m")
                        versionName = m.group("m").replace("v1.0.", "")
                    }
                }
            }
            if (localVersion.toLong() < versionName.toLong()) {
                apkUrl = apkUrl + outputFile
                mHandler.sendEmptyMessage(SHOWDOWN)
            } else {
                return@Runnable
            }
        }).start()
    }


    /**
     * 从服务器下载APK安装包
     */
    public fun DownloadApk() {
        downLoadThread = Thread(DownApkWork)
        downLoadThread!!.start()
    }

    val DownApkWork=Runnable {
         //fun run() {
            var url:URL?=null
            try {
                //如果下载地址是HTTPS,则把这段加上,http则不需要
                val sslContext:SSLContext  = SSLContext.getInstance("SSL") //第一个参数为 返回实现指定安全套接字协议的SSLContext对象。第二个为提供者
                val tm: Array<MyX509TrustManager> = arrayOf(MyX509TrustManager())
                sslContext.init(null, tm,  SecureRandom());
                var ssf:SSLSocketFactory  = sslContext.getSocketFactory()


                url = URL(apkUrl)
                val conn:HttpURLConnection  =
                    url.openConnection() as HttpURLConnection
                conn.connect()
                var length:Int  = conn.getContentLength()
                val ins: InputStream = conn.getInputStream()
                val fos: FileOutputStream = FileOutputStream(apkFile)
                var count:Int  = 0
                var buf=ByteArray(1024)
                while (!intercept) {
                    var numread:Int  = ins.read(buf)
                    count += numread
                    progress = ((count.toFloat() / length) * 100).toInt()
                    // 下载进度
                    mHandler.sendEmptyMessage(DOWN_UPDATE);
                    //Log.i("numread",numread.toString())
                    if (numread <= 0) {
                        // 下载完成通知安装
                        Log.i("numread","下载完成")
                        mHandler.sendEmptyMessage(DOWN_OVER);
                        break;
                    }
                    fos.write(buf, 0, numread);
                }
                fos.close();
                ins.close();

            } catch (e:Exception ) {
                e.printStackTrace();
                Log.e("DownApkWork_Error",e.message.toString())
            }
        //}

    }

    /**
     * 安装APK内容
     */
    public fun installAPK() {
        try {
            if (!apkFile.exists()) {
                return
            }

            val intent:Intent  = Intent(Intent.ACTION_VIEW)
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//安装完成后打开新版本
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 给目标应用一个临时授权
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {//判断版本大于等于7.0
                //如果SDK版本>=24,即:Build.VERSION.SDK_INT >= 24,使用FileProvider兼容安装apk
                val packageName:String  = mContext.getApplicationContext().getPackageName()
                val authority:String  = StringBuilder(packageName).append(".file-provider").toString();
                val apkUri: Uri = FileProvider.getUriForFile(mContext, authority, apkFile);
                intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
            } else {
                intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
            }
            mContext.startActivity(intent);
            android.os.Process.killProcess(android.os.Process.myPid());//安装完之后会提示”完成” “打开”。


        } catch (e:Exception ) {
            Log.e("installAPK",e.message.toString())
        }
    }

    private val mHandler: Handler = object : Handler() {
        override fun handleMessage(msg: Message) {
            when (msg.what) {
                SHOWDOWN -> ShowUpdateDialog()
                DOWN_UPDATE -> {
                    txtStatus!!.text = "$progress%"
                    mProgress!!.progress = progress
                }

                DOWN_OVER -> {
                    Toast.makeText(mContext, "下载完毕", Toast.LENGTH_SHORT).show()
                    installAPK()
                }

                else -> {}
            }
        }
    }

    public fun doGet(httpurl:String): String {
        var connection:HttpURLConnection?= null;
        var ipts:InputStream?= null;
        var br:BufferedReader?= null;
        var result:String  = "";
        try {
            val url:URL  = URL(httpurl);
            connection = url.openConnection() as HttpURLConnection;
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(15000);
            connection.setReadTimeout(60000);
            connection.connect();
            if (connection.getResponseCode() == 200) {
                ipts = connection.getInputStream();
                br = BufferedReader(InputStreamReader(ipts, "UTF-8"));
                var sbf:StringBuffer  = StringBuffer();
                var temp:String ?= null;
                while ((br.readLine().also { temp = it }) != null) {
                    sbf.append(temp);
                    sbf.append("\r\n");
                }
                result = sbf.toString();
            }
        } catch (e: MalformedURLException) {
            e.printStackTrace();
        } catch (e: IOException) {
            e.printStackTrace();
        } finally {
            if (null != br) {
                try {
                    br.close();
                } catch (e:IOException ) {
                    e.printStackTrace();
                }
            }
            if (null != ipts) {
                try {
                    ipts.close();
                } catch (e:IOException ) {
                    e.printStackTrace();
                }
            }

            connection!!.disconnect();
        }
        return result;
    }
}

创建MyX509TrustManager.kt

package com.zwb.a2mesapp

import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager


class MyX509TrustManager : X509TrustManager {
    @Throws(CertificateException::class)
    override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
        // TODO Auto-generated method stub
    }

    @Throws(CertificateException::class)
    override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
        // TODO Auto-generated method stub
    }

    override fun getAcceptedIssuers(): Array<X509Certificate>? {
        // TODO Auto-generated method stub
        return null
    }
}

二、修改AndroidManifest.xml,增加权限申请和文件路径配置信息

注意:provider节点是在application节点之内

<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 在SDCard中创建与删除文件权限 -->
<uses-permission
    android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
    tools:ignore="ProtectedPermissions" />
<!-- 存储权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 安装APK权限 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

<!--安装apk时用于定位安装包-->
<provider
    android:name=".MyFileProvider"
    android:authorities="${applicationId}.file-provider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

三、建立资源文件

1、新增file_paths.xml

在res的xml文件夹下创建一个名为file_paths.xml的文件

file_paths.xml代码内容如下

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <root-path name="root_path" path="."></root-path>
    <!--安装包文件存储路径-->
    <external-path
        name="my_download"
        path="Download/" />
</paths>

2、新增network_security_config.xml

在AndroidManifest.xml中配置了networkSecurityConfig,所以要新建一个对应的xml

networkSecurityConfig.xml内容如下,这里的含义是"是否允许网络通信使用明文网络流量",配置后安卓应用就可以使用http协议进行通信

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

拓展:从Android9.0开始增加了对http请求的限制,所以需要我们另外创建安全配置文件

四、在MainActivity.ky的onCreate事件中添加检查更新的逻辑

//检查更新
try {
    //6.0才用动态权限
    if (Build.VERSION.SDK_INT >= 23) {

        val permissions = arrayOf<String>(
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.ACCESS_WIFI_STATE,
            Manifest.permission.INTERNET
        )
        val permissionList: MutableList<String> = ArrayList()
        for (i in permissions.indices) {
            if (ActivityCompat.checkSelfPermission(
                    this,
                    permissions[i]
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                permissionList.add(permissions[i])
            }
        }
        if (permissionList.size <= 0) {
            //说明权限都已经通过,可以做你想做的事情去
            //自动更新
            val manager = AutoUpdater(this@MainActivity)
            manager.CheckUpdate()
        } else {
            //存在未允许的权限
            ActivityCompat.requestPermissions(this, permissions, 100)
        }
    }
} catch (ex: Exception) {
    Toast.makeText(this@MainActivity, "自动更新异常:" + ex.message, Toast.LENGTH_SHORT)
        .show()
}

效果图

下载后的文件会保存在Download文件夹内,这个文件夹就是在file_paths.xml中配置的路径。文件名my.apk是AutoUpdate.kt类中定义的保存文件名。这两个都可以根据需要自行定义

-------------------------------------------------------------补充于2024-07-23-------------------------------------------

 MyFileProvider.kt的代码如下

package com.zwb.a2mesapp

import androidx.core.content.FileProvider

class MyFileProvider: FileProvider() {
}

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值