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:
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user