StandAPP/app/src/main/java/com/stand/standapp/UpdateManager.kt

302 lines
12 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.stand.standapp
import android.app.ProgressDialog
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import timber.log.Timber
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.stand.standapp.net.NetworkModule
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
class UpdateManager(private val context: Context) {
private var progressDialog: ProgressDialog? = null
private val handler = Handler(Looper.getMainLooper())
private var call: Call? = null
private val fileName = "StandApp_Update.apk"
/**
* 获取应用外部专属下载目录下的文件 URI避免申请外部存储权限的同时确保安装包能被系统安装器读取
*/
private fun getCacheFileUri(): Uri {
val dir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
?: context.externalCacheDir
?: context.cacheDir
val file = File(dir, fileName)
if (file.exists()) {
file.delete()
}
return Uri.fromFile(file)
}
fun checkUpdate() {
val serverUrl = AppConfig.getServerUrl(context)
val currentVersionCode = try {
context.packageManager.getPackageInfo(context.packageName, 0).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) it.longVersionCode.toInt() else it.versionCode
}
} catch (e: Exception) { 0 }
val currentVersionName = try {
context.packageManager.getPackageInfo(context.packageName, 0).versionName
} catch (e: Exception) { "" }
// 获取详细设备和尺寸属性
val deviceId = android.provider.Settings.Secure.getString(context.contentResolver, android.provider.Settings.Secure.ANDROID_ID) ?: "unknown_android"
val displayMetrics = context.resources.displayMetrics
val screenWidth = displayMetrics.widthPixels
val screenHeight = displayMetrics.heightPixels
val screenDensity = displayMetrics.density.toString()
val loginAccount = AppConfig.getSavedUsername(context)
val jsonParams = JSONObject().apply {
put("versionCode", currentVersionCode)
put("versionName", currentVersionName)
put("deviceId", deviceId)
put("deviceType", "接警终端")
put("deviceBrand", Build.BRAND)
put("deviceModel", Build.MODEL)
put("osVersion", "Android " + Build.VERSION.RELEASE)
put("screenWidth", screenWidth)
put("screenHeight", screenHeight)
put("screenDensity", screenDensity)
put("loginAccount", loginAccount)
}
val url = "$serverUrl/api/app-version/check-update"
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = jsonParams.toString().toRequestBody(mediaType)
val client = NetworkModule.defaultClient
val request = Request.Builder()
.url(url)
.post(requestBody)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Timber.tag("UpdateManager").e(e, "检查更新失败")
}
override fun onResponse(call: Call, response: Response) {
try {
val body = response.body?.string() ?: throw IOException("返回内容为空")
if (!response.isSuccessful) {
handler.post { Toast.makeText(context.applicationContext, "更新检查失败: ${response.code}", Toast.LENGTH_SHORT).show() }
return
}
val json = JSONObject(body)
if (json.getInt("code") == 0) {
val data = json.optJSONObject("data")
if (data != null) {
val newVersionCode = data.optInt("versionCode", 0)
if (newVersionCode > currentVersionCode) {
val versionName = data.optString("versionName", "New")
val content = data.optString("updateContent", "优化系统体验")
val id = data.optString("id", "")
if (id != "") {
(context as? AppCompatActivity)?.runOnUiThread {
try {
showUpdateDialog(versionName, content, id)
} catch (e: Exception) {
Toast.makeText(context.applicationContext, "显示更新弹窗失败", Toast.LENGTH_SHORT).show()
}
}
}
} else {
handler.post { Toast.makeText(context.applicationContext, "当前已是最新版本", Toast.LENGTH_SHORT).show() }
}
} else {
handler.post { Toast.makeText(context.applicationContext, "当前已是最新版本", Toast.LENGTH_SHORT).show() }
}
} else {
val msg = json.optString("msg", "未知错误")
handler.post { Toast.makeText(context.applicationContext, "更新服务提示: $msg", Toast.LENGTH_SHORT).show() }
}
} catch (e: Exception) {
Timber.tag("UpdateManager").e(e, "解析或处理更新结果失败")
handler.post { Toast.makeText(context.applicationContext, "更新信息解析失败", Toast.LENGTH_SHORT).show() }
}
}
})
}
private fun showUpdateDialog(versionName: String, content: String, id: String) {
val view = (context as AppCompatActivity).layoutInflater.inflate(R.layout.dialog_update, null)
val tvTitle = view.findViewById<android.widget.TextView>(R.id.tv_update_title)
val tvContent = view.findViewById<android.widget.TextView>(R.id.tv_update_content)
tvTitle.text = "新版本 V$versionName"
tvContent.text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
android.text.Html.fromHtml(content, android.text.Html.FROM_HTML_MODE_LEGACY)
} else {
@Suppress("DEPRECATION")
android.text.Html.fromHtml(content)
}
AlertDialog.Builder(context)
.setView(view)
.setCancelable(false)
.setPositiveButton("立即升级") { _, _ ->
val serverUrl = AppConfig.getServerUrl(context)
val downloadUrl = "$serverUrl/api/app-version/download/$id"
startManualDownload(downloadUrl)
}
.setNegativeButton("稍后再说", null)
.show()
}
private fun startManualDownload(url: String) {
showProgressDialog()
val client = NetworkModule.defaultClient
val request = Request.Builder()
.url(url)
.addHeader("User-Agent", "Mozilla/5.0 (Android)")
.build()
call = client.newCall(request)
call?.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
handler.post {
progressDialog?.dismiss()
Toast.makeText(context.applicationContext, "连接服务器失败", Toast.LENGTH_SHORT).show()
}
}
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
handler.post {
progressDialog?.dismiss()
Toast.makeText(context.applicationContext, "下载失败: ${response.code}", Toast.LENGTH_SHORT).show()
}
return
}
val body = response.body
if (body != null) {
try {
saveFileToPublic(body)
} catch (e: Exception) {
handler.post {
progressDialog?.dismiss()
Toast.makeText(context.applicationContext, "保存失败", Toast.LENGTH_SHORT).show()
}
}
}
}
})
}
private fun saveFileToPublic(body: ResponseBody) {
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
var targetUri: Uri? = null
try {
targetUri = getCacheFileUri()
val file = File(targetUri.path!!)
outputStream = FileOutputStream(file)
inputStream = body.byteStream()
val totalBytes = body.contentLength()
val buffer = ByteArray(8192)
var bytesRead: Int
var totalRead: Long = 0
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
totalRead += bytesRead
if (totalBytes > 0) {
val progress = (totalRead * 100 / totalBytes).toInt()
handler.post {
progressDialog?.progress = progress
progressDialog?.setMessage("已下载: $progress%")
}
}
}
outputStream.flush()
handler.post {
progressDialog?.dismiss()
Toast.makeText(context.applicationContext, "下载完成,正在安装...", Toast.LENGTH_SHORT).show()
installApk(targetUri)
}
} catch (e: Exception) {
handler.post {
progressDialog?.dismiss()
Toast.makeText(context.applicationContext, "保存失败: ${e.message}", Toast.LENGTH_LONG).show()
}
} finally {
inputStream?.close()
outputStream?.close()
}
}
private fun showProgressDialog() {
progressDialog = ProgressDialog(context).apply {
setTitle("正在下载")
setMessage("正在连接服务器...")
setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
max = 100
setCancelable(false)
setButton(ProgressDialog.BUTTON_NEGATIVE, "取消下载") { _, _ ->
call?.cancel()
}
show()
}
}
private fun installApk(uri: Uri) {
try {
// Android 8.0+ 检查安装权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!context.packageManager.canRequestPackageInstalls()) {
val intent = Intent(android.provider.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
intent.data = Uri.parse("package:" + context.packageName)
context.startActivity(intent)
Toast.makeText(context.applicationContext, "请先授予安装权限", Toast.LENGTH_SHORT).show()
return
}
}
val intent = Intent(Intent.ACTION_VIEW)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
// 如果是 content:// URI 直接使用,否则通过 FileProvider 转换
val installUri = if (uri.scheme == "content") {
uri
} else {
val file = File(uri.path!!)
FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
}
intent.setDataAndType(installUri, "application/vnd.android.package-archive")
context.startActivity(intent)
} catch (e: Exception) {
Timber.tag("UpdateManager").e(e, "安装失败")
Toast.makeText(context.applicationContext, "无法自动调起安装,请在系统下载目录中手动安装", Toast.LENGTH_LONG).show()
}
}
}