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(R.id.tv_update_title) val tvContent = view.findViewById(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() } } }