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() {
}