302 lines
12 KiB
Kotlin
302 lines
12 KiB
Kotlin
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()
|
||
}
|
||
}
|
||
}
|