feat(activity): 添加 Zip 文件下载解压工具和趣味页面

- 新增 ZipDownloadUtils:支持大文件下载、进度回调、断点续传
- 新增 ZipExtractUtils:支持 Zip 文件解压、进度回调、自动创建目录
- 新增 ModelResourceManager:高层资源管理器,协调下载和解压流程
- 新增 FunActivity:趣味测试页面,包含 3 个按钮
  - 按钮 1:下载 Zip 文件
  - 按钮 2:解压 Zip 文件
  - 按钮 3:读取 JSON 并保存到 ObjectBox 数据库
- 新增 activity_fun.xml:趣味页面布局
- 新增 ZipToolsUsageExample:工具使用示例文档
- 改进 ToastUtils:改为 Context 扩展方法 (toast/toastLong)
- 暂时注释 HomeActivity 中 tvTime 的点击事件
This commit is contained in:
2026-03-13 11:27:53 +08:00
parent baee019444
commit c697b4aa65
13 changed files with 1211 additions and 58 deletions
@@ -0,0 +1,298 @@
package com.sw.inbound.activity
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.sw.inbound.base.BaseActivity
import com.sw.inbound.databinding.ActivityFunBinding
import com.sw.inbound.objbox.Food
import com.sw.inbound.objbox.FoodClassInfo
import com.sw.inbound.objbox.ObjectBox
import com.sw.inbound.utils.toast
import com.sw.inbound.utils.ZipDownloadUtils
import com.sw.inbound.utils.ZipExtractUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
/**
* 趣味页面 Activity
* 用于测试 Zip 文件下载、解压和数据库操作
*/
@AndroidEntryPoint
class FunActivity : BaseActivity() {
private lateinit var binding: ActivityFunBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityFunBinding.inflate(layoutInflater)
setContentView(binding.root)
initViews()
}
/**
* 初始化各控件的点击事件
*/
private fun initViews() {
// 返回按钮点击事件
binding.ivBack.setOnClickListener {
finish()
}
// 下载按钮
binding.btnDownload.setOnClickListener {
downloadZipFile()
}
// 解压按钮
binding.btnExtract.setOnClickListener {
extractZipFile()
}
// 读取并保存按钮
binding.btnLoadAndSave.setOnClickListener {
loadAndSaveToDatabase()
}
}
/**
* 下载 Zip 文件
*/
private fun downloadZipFile() {
lifecycleScope.launch {
try {
updateProgress("开始下载...")
binding.btnDownload.isEnabled = false
val zipFile = File(cacheDir, "model_resources.zip")
// 假的下载地址
val downloadUrl = "https://dev.yixiong-tech.com:8081/common/files/20260313/embedding_model_resources.zip"
withContext(Dispatchers.IO) {
ZipDownloadUtils.downloadZip(
downloadUrl = downloadUrl,
targetFile = zipFile,
listener = object : ZipDownloadUtils.DownloadProgressListener {
override fun onProgress(currentSize: Long, totalSize: Long, progress: Int) {
updateProgress("下载中... $progress%")
}
override fun onSuccess(file: File) {
updateProgress("下载完成!文件大小: ${formatFileSize(file.length())}")
Timber.d("Zip 文件下载完成: ${file.absolutePath}")
}
override fun onFailure(exception: Exception) {
updateProgress("下载失败: ${exception.message}")
Timber.e(exception, "下载失败")
this@FunActivity.toast("下载失败: ${exception.message}")
}
}
)
}
} catch (e: Exception) {
Timber.e(e, "下载异常")
updateProgress("异常: ${e.message}")
this@FunActivity.toast("异常: ${e.message}")
} finally {
binding.btnDownload.isEnabled = true
}
}
}
/**
* 解压 Zip 文件
*/
private fun extractZipFile() {
lifecycleScope.launch {
try {
updateProgress("开始解压...")
binding.btnExtract.isEnabled = false
val zipFile = File(cacheDir, "model_resources.zip")
val targetDir = File(cacheDir, "model_resources")
if (!zipFile.exists()) {
updateProgress("Zip 文件不存在,请先下载")
this@FunActivity.toast("Zip 文件不存在,请先下载")
return@launch
}
withContext(Dispatchers.IO) {
ZipExtractUtils.extractZip(
zipFile = zipFile,
targetDir = targetDir,
listener = object : ZipExtractUtils.ExtractProgressListener {
override fun onProgress(
currentFile: String,
currentIndex: Int,
totalFiles: Int,
progress: Int
) {
updateProgress("解压中... $progress% ($currentIndex/$totalFiles)")
}
override fun onSuccess(targetDir: File) {
updateProgress("解压完成!目录: ${targetDir.absolutePath}")
Timber.d("Zip 文件解压完成: ${targetDir.absolutePath}")
}
override fun onFailure(exception: Exception) {
updateProgress("解压失败: ${exception.message}")
Timber.e(exception, "解压失败")
this@FunActivity.toast("解压失败: ${exception.message}")
}
}
)
}
} catch (e: Exception) {
Timber.e(e, "解压异常")
updateProgress("异常: ${e.message}")
this@FunActivity.toast("异常: ${e.message}")
} finally {
binding.btnExtract.isEnabled = true
}
}
}
/**
* 读取解压后的文件并保存到 ObjectBox 数据库
*/
private fun loadAndSaveToDatabase() {
lifecycleScope.launch {
try {
updateProgress("开始读取数据...")
binding.btnLoadAndSave.isEnabled = false
val resourcesDir = File(cacheDir, "model_resources")
if (!resourcesDir.exists()) {
updateProgress("资源目录不存在,请先解压")
this@FunActivity.toast("资源目录不存在,请先解压")
return@launch
}
withContext(Dispatchers.IO) {
// 清除已有数据
updateProgress("清除已有数据...")
clearFoodData()
// 读取 JSON 文件
updateProgress("读取 JSON 文件...")
val embeddingsJson = readFileContent(File(resourcesDir, "data/embeddings.json"))
val labelsJson = readFileContent(File(resourcesDir, "data/labels.json"))
val classInfoJson = readFileContent(File(resourcesDir, "data/class_info.json"))
if (embeddingsJson.isEmpty() || labelsJson.isEmpty() || classInfoJson.isEmpty()) {
updateProgress("JSON 文件读取失败")
this@FunActivity.toast("JSON 文件读取失败")
return@withContext
}
// 解析 JSON 数据
updateProgress("解析 JSON 数据...")
val embeddingsList: List<List<Float>> =
Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
val labelsList: IntArray = Gson().fromJson(labelsJson, IntArray::class.java)
val classInfo: FoodClassInfo =
Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
// 构建 Food 对象列表
updateProgress("构建数据对象...")
val foodMap = classInfo.idx_to_class
val foodList = mutableListOf<Food>()
val size = embeddingsList.size
Timber.d("开始构建 Food 对象,总数: $size")
embeddingsList.forEachIndexed { index, floatList ->
val classIdx = labelsList[index]
val foodName = foodMap["$classIdx"]
val array = floatList.toFloatArray()
val food = Food(name = foodName, foodVector = array, foodIdx = -1)
foodList.add(food)
// 每 100 条更新一次进度
if ((index + 1) % 100 == 0) {
val progress = ((index + 1) * 100) / size
updateProgress("构建数据中... $progress% (${index + 1}/$size)")
}
}
// 保存到数据库
updateProgress("保存到数据库...")
ObjectBox.putAll(foodList)
updateProgress("完成!已保存 ${foodList.size} 条数据到数据库")
Timber.d("数据保存完成,总数: ${foodList.size}")
}
} catch (e: Exception) {
Timber.e(e, "读取和保存异常")
updateProgress("异常: ${e.message}")
this@FunActivity.toast("异常: ${e.message}")
} finally {
binding.btnLoadAndSave.isEnabled = true
}
}
}
/**
* 清除 Food 表中的所有数据
*/
private suspend fun clearFoodData() {
try {
ObjectBox.safeDbOp {
val box = ObjectBox.getBox<Food>()
box?.removeAll()
Timber.d("Food 表数据已清除")
}
} catch (e: Exception) {
Timber.e(e, "清除数据失败")
}
}
/**
* 读取文件内容
*/
private fun readFileContent(file: File): String {
return try {
if (!file.exists()) {
Timber.w("文件不存在: ${file.absolutePath}")
""
} else {
file.readText()
}
} catch (e: Exception) {
Timber.e(e, "读取文件失败: ${file.absolutePath}")
""
}
}
/**
* 更新进度显示
*/
private fun updateProgress(message: String) {
runOnUiThread {
binding.tvProgress.text = message
Timber.d("进度: $message")
}
}
/**
* 格式化文件大小
*/
private fun formatFileSize(bytes: Long): String {
return when {
bytes <= 0 -> "0 B"
bytes < 1024 -> "$bytes B"
bytes < 1024 * 1024 -> String.format("%.2f KB", bytes / 1024.0)
bytes < 1024 * 1024 * 1024 -> String.format("%.2f MB", bytes / (1024.0 * 1024))
else -> String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024))
}
}
}