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,52 @@
|
|||||||
|
请按以下步骤为当前改动生成 Git 提交信息并执行提交:
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
1. 并行运行以下命令,了解当前改动:
|
||||||
|
- `git diff --staged` 查看已暂存的变更内容
|
||||||
|
- `git diff` 查看未暂存的变更内容
|
||||||
|
- `git status` 查看变更文件列表
|
||||||
|
|
||||||
|
2. 若存在未暂存的文件,询问用户是否需要一起提交。
|
||||||
|
|
||||||
|
3. 根据变更内容,按以下规范生成提交信息:
|
||||||
|
|
||||||
|
**格式:** `<类型>(<范围>): <简短描述>`
|
||||||
|
|
||||||
|
| 类型 | 适用场景 |
|
||||||
|
|------|---------|
|
||||||
|
| feat | 新增功能 |
|
||||||
|
| fix | 修复 bug |
|
||||||
|
| refactor | 代码重构(不改变行为) |
|
||||||
|
| chore | 构建/配置/依赖调整 |
|
||||||
|
| docs | 文档更新 |
|
||||||
|
| perf | 性能优化 |
|
||||||
|
|
||||||
|
**范围参考本项目模块:**
|
||||||
|
- `face` — 人脸识别相关(LoginByFaceActivity、RecognizeViewModel)
|
||||||
|
- `serial` — 串口/开柜相关(SerialApi、PlateUtils)
|
||||||
|
- `network` — 网络层(ApiService、RemoteRepository、RequestInterceptor)
|
||||||
|
- `activity` — Activity 界面逻辑
|
||||||
|
- `fragment` — Fragment 界面逻辑
|
||||||
|
- `viewmodel` — ViewModel 层
|
||||||
|
- `ui` — 布局/样式调整
|
||||||
|
- `global` — GlobalData、MyApp 等全局配置
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
- `feat(face): 新增识别失败后自动采集人脸流程`
|
||||||
|
- `fix(serial): 修复开柜编号解析为负数导致串口指令异常`
|
||||||
|
- `refactor(network): 统一 API 地址使用 GlobalData.appBaseUrl 前缀`
|
||||||
|
|
||||||
|
4. 向用户展示拟提交的信息,确认后执行:
|
||||||
|
```
|
||||||
|
git add <相关文件>
|
||||||
|
git commit -m "<提交信息>"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 提交完成后运行 `git log --oneline -3` 确认结果。
|
||||||
|
|
||||||
|
## 约束
|
||||||
|
|
||||||
|
- 提交信息必须使用**简体中文**描述
|
||||||
|
- 不要使用 `--no-verify` 跳过钩子
|
||||||
|
- 不要在未经用户确认的情况下执行 `git push`
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
在本项目中新增一个后端 API 接口,需要同时修改三层:ApiService → RemoteRepository → ViewModel。
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
### 1. 收集信息
|
||||||
|
询问用户以下信息(如已在指令中提供则跳过):
|
||||||
|
- 接口名称/功能描述
|
||||||
|
- 请求方式:GET / POST / POST + FormUrlEncoded / Multipart
|
||||||
|
- 接口路径(相对路径,如 `/sideboard/app/xxx`)
|
||||||
|
- 请求参数字段和类型
|
||||||
|
- 响应数据类型(已有类 or 需新建)
|
||||||
|
- 归属哪个 ViewModel:`SettingViewModel` / `UserViewModel` / `DeviceViewModel`
|
||||||
|
|
||||||
|
### 2. 创建数据模型(如需要)
|
||||||
|
**请求参数类:** `app/src/main/java/com/sw/platecabinet/model/request/<XxxParam>.kt`
|
||||||
|
**响应数据类:** `app/src/main/java/com/sw/platecabinet/model/response/<XxxModel>.kt`
|
||||||
|
|
||||||
|
数据类规范:
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* <描述>
|
||||||
|
*/
|
||||||
|
data class XxxParam(
|
||||||
|
val fieldName: String // 字段说明
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 在 ApiService 中添加接口方法
|
||||||
|
文件:`app/src/main/java/com/sw/platecabinet/network/api/ApiService.kt`
|
||||||
|
|
||||||
|
**关键规则:**
|
||||||
|
- 必须使用 `@Url` 注解,URL 格式为:`"${GlobalData.appBaseUrl}/terminal/neglect/<路径>"`
|
||||||
|
- 不要使用相对路径(baseUrl 拦截器不适用于本项目)
|
||||||
|
- 方法必须为 `suspend` 函数,返回 `ApiResponse<T>`
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* <接口功能描述>
|
||||||
|
*/
|
||||||
|
@POST // 或 @GET / @FormUrlEncoded @POST / @Multipart @POST
|
||||||
|
suspend fun <methodName>(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/<路径>",
|
||||||
|
@Body param: XxxParam // 或 @Query / @Field / @Part
|
||||||
|
): ApiResponse<XxxModel?>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 在 RemoteRepository 中添加调用方法
|
||||||
|
文件:`app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt`
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* <功能描述>
|
||||||
|
*/
|
||||||
|
suspend fun <methodName>(/* 参数 */): ApiResponse<XxxModel?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.<methodName>(/* 参数传递 */)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
约束:
|
||||||
|
- 必须用 `safeApiCall { }` 包裹,不要裸调用 apiService
|
||||||
|
- 参数直接透传,不在 Repository 层做业务逻辑
|
||||||
|
|
||||||
|
### 5. 在 ViewModel 中添加调用逻辑
|
||||||
|
文件:`app/src/main/java/com/sw/platecabinet/viewmodel/<XxxViewModel>.kt`
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* <功能描述>
|
||||||
|
* @param callback 成功时回调,参数为响应数据
|
||||||
|
*/
|
||||||
|
fun <methodName>(/* 业务参数 */, callback: (XxxModel?) -> Unit) {
|
||||||
|
launch { // 不需要 loading 时用 launch;需要 loading 时用 launchWithLoading
|
||||||
|
val response = repository.<methodName>(/* 参数 */)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
runOnMainThread { callback(response.result) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意:** `launch` / `launchWithLoading` 在协程中执行,回调若需更新 UI 需切换到主线程(使用 `withContext(Dispatchers.Main)` 或在 Activity 中用 `runOnUiThread`)。
|
||||||
|
|
||||||
|
### 6. 在 Activity/Fragment 中调用
|
||||||
|
```kotlin
|
||||||
|
viewModel.<methodName>(/* 参数 */) { result ->
|
||||||
|
// 处理结果
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
- 所有注释使用简体中文
|
||||||
|
- 向用户展示将要修改的文件清单(含 Diff 预览),等待确认后再写文件
|
||||||
|
- `RequestInterceptor` 已自动注入认证 header,无需在方法中手动添加
|
||||||
|
- 响应体统一为 `ApiResponse<T>`,通过 `response.isSuccess()` 和 `response.result` 取数据
|
||||||
@@ -147,7 +147,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation("io.github.scwang90:refresh-layout-kernel:3.0.0-alpha")
|
implementation("io.github.scwang90:refresh-layout-kernel:3.0.0-alpha")
|
||||||
implementation("io.github.scwang90:refresh-header-classics:3.0.0-alpha")
|
implementation("io.github.scwang90:refresh-header-classics:3.0.0-alpha")
|
||||||
val objectboxVersion = "5.0.1"
|
val objectboxVersion:String by rootProject.extra
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
|
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -59,6 +59,8 @@
|
|||||||
android:windowSoftInputMode="adjustPan" />
|
android:windowSoftInputMode="adjustPan" />
|
||||||
<activity android:name="com.sw.inbound.activity.LocalImagePreviewActivity" />
|
<activity android:name="com.sw.inbound.activity.LocalImagePreviewActivity" />
|
||||||
<activity android:name="com.sw.inbound.activity.PurchaseOrderActivity" />
|
<activity android:name="com.sw.inbound.activity.PurchaseOrderActivity" />
|
||||||
|
<activity android:name="com.sw.inbound.activity.FunActivity"
|
||||||
|
android:screenOrientation="landscape" />
|
||||||
<!-- 首页 Activity(XML 版本,对应 HomeScreen.kt Compose 版本) -->
|
<!-- 首页 Activity(XML 版本,对应 HomeScreen.kt Compose 版本) -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.sw.inbound.MainActivity"
|
android:name="com.sw.inbound.MainActivity"
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,11 @@ class HomeActivity : BaseActivity() {
|
|||||||
startActivity<FoodCollectionActivity>()
|
startActivity<FoodCollectionActivity>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 时间点击:跳转趣味页面
|
||||||
|
// binding.tvTime.setOnClickListener {
|
||||||
|
// startActivity<FunActivity>()
|
||||||
|
// }
|
||||||
|
|
||||||
// 采购单入库入口
|
// 采购单入库入口
|
||||||
binding.ivOrderPurchase.setOnClickListener {
|
binding.ivOrderPurchase.setOnClickListener {
|
||||||
startActivity<PurchaseOrderActivity>()
|
startActivity<PurchaseOrderActivity>()
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package com.sw.inbound.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型资源管理器
|
||||||
|
* 负责从接口下载模型资源包(Zip),并解压到应用缓存目录
|
||||||
|
* 替代原有的 assets 中写死的 data 文件夹和 .pt 文件
|
||||||
|
*/
|
||||||
|
object ModelResourceManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源下载和解压的回调接口
|
||||||
|
*/
|
||||||
|
interface ResourceCallback {
|
||||||
|
/**
|
||||||
|
* 下载进度回调
|
||||||
|
* @param progress 进度百分比 (0-100)
|
||||||
|
* @param message 进度信息
|
||||||
|
*/
|
||||||
|
fun onDownloadProgress(progress: Int, message: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压进度回调
|
||||||
|
* @param progress 进度百分比 (0-100)
|
||||||
|
* @param message 进度信息
|
||||||
|
*/
|
||||||
|
fun onExtractProgress(progress: Int, message: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成回调
|
||||||
|
* @param resourceDir 资源所在目录
|
||||||
|
*/
|
||||||
|
fun onSuccess(resourceDir: File)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败回调
|
||||||
|
* @param errorMessage 错误信息
|
||||||
|
*/
|
||||||
|
fun onFailure(errorMessage: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载并解压模型资源
|
||||||
|
* @param context 应用上下文
|
||||||
|
* @param downloadUrl 资源包下载链接
|
||||||
|
* @param callback 进度和结果回调
|
||||||
|
*/
|
||||||
|
fun downloadAndExtractResources(
|
||||||
|
context: Context,
|
||||||
|
downloadUrl: String,
|
||||||
|
callback: ResourceCallback
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// 获取缓存目录
|
||||||
|
val cacheDir = context.cacheDir
|
||||||
|
val resourcesDir = File(cacheDir, "model_resources")
|
||||||
|
val zipFile = File(cacheDir, "model_resources.zip")
|
||||||
|
|
||||||
|
Timber.d("资源缓存目录: ${resourcesDir.absolutePath}")
|
||||||
|
Timber.d("Zip 文件路径: ${zipFile.absolutePath}")
|
||||||
|
|
||||||
|
// Step 1: 下载 Zip 文件
|
||||||
|
Timber.d("开始下载资源包: $downloadUrl")
|
||||||
|
ZipDownloadUtils.downloadZip(
|
||||||
|
downloadUrl = downloadUrl,
|
||||||
|
targetFile = zipFile,
|
||||||
|
listener = object : ZipDownloadUtils.DownloadProgressListener {
|
||||||
|
override fun onProgress(currentSize: Long, totalSize: Long, progress: Int) {
|
||||||
|
val message = "下载中... ${formatFileSize(currentSize)}/${formatFileSize(totalSize)}"
|
||||||
|
callback.onDownloadProgress(progress, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(file: File) {
|
||||||
|
Timber.d("资源包下载完成")
|
||||||
|
callback.onDownloadProgress(100, "下载完成")
|
||||||
|
|
||||||
|
// Step 2: 解压 Zip 文件
|
||||||
|
extractResources(resourcesDir, zipFile, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(exception: Exception) {
|
||||||
|
val errorMsg = "资源包下载失败: ${exception.message}"
|
||||||
|
Timber.e(exception, errorMsg)
|
||||||
|
callback.onFailure(errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val errorMsg = "资源下载和解压过程异常: ${e.message}"
|
||||||
|
Timber.e(e, errorMsg)
|
||||||
|
callback.onFailure(errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压资源包
|
||||||
|
*/
|
||||||
|
private fun extractResources(
|
||||||
|
resourcesDir: File,
|
||||||
|
zipFile: File,
|
||||||
|
callback: ResourceCallback
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
Timber.d("开始解压资源包")
|
||||||
|
ZipExtractUtils.extractZip(
|
||||||
|
zipFile = zipFile,
|
||||||
|
targetDir = resourcesDir,
|
||||||
|
listener = object : ZipExtractUtils.ExtractProgressListener {
|
||||||
|
override fun onProgress(
|
||||||
|
currentFile: String,
|
||||||
|
currentIndex: Int,
|
||||||
|
totalFiles: Int,
|
||||||
|
progress: Int
|
||||||
|
) {
|
||||||
|
val message = "解压中... ($currentIndex/$totalFiles) $currentFile"
|
||||||
|
callback.onExtractProgress(progress, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(targetDir: File) {
|
||||||
|
Timber.d("资源包解压完成: ${targetDir.absolutePath}")
|
||||||
|
callback.onExtractProgress(100, "解压完成")
|
||||||
|
|
||||||
|
// 清理 Zip 文件
|
||||||
|
zipFile.delete()
|
||||||
|
Timber.d("临时 Zip 文件已删除")
|
||||||
|
|
||||||
|
callback.onSuccess(targetDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(exception: Exception) {
|
||||||
|
val errorMsg = "资源包解压失败: ${exception.message}"
|
||||||
|
Timber.e(exception, errorMsg)
|
||||||
|
callback.onFailure(errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val errorMsg = "解压过程异常: ${e.message}"
|
||||||
|
Timber.e(e, errorMsg)
|
||||||
|
callback.onFailure(errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已缓存的资源目录
|
||||||
|
* @param context 应用上下文
|
||||||
|
* @return 资源目录,如果不存在则返回 null
|
||||||
|
*/
|
||||||
|
fun getCachedResourcesDir(context: Context): File? {
|
||||||
|
val resourcesDir = File(context.cacheDir, "model_resources")
|
||||||
|
return if (resourcesDir.exists() && resourcesDir.isDirectory) {
|
||||||
|
resourcesDir
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理缓存的资源
|
||||||
|
* @param context 应用上下文
|
||||||
|
* @return 是否清理成功
|
||||||
|
*/
|
||||||
|
fun clearCachedResources(context: Context): Boolean {
|
||||||
|
return try {
|
||||||
|
val resourcesDir = File(context.cacheDir, "model_resources")
|
||||||
|
val zipFile = File(context.cacheDir, "model_resources.zip")
|
||||||
|
|
||||||
|
val dirDeleted = resourcesDir.deleteRecursively()
|
||||||
|
val zipDeleted = zipFile.delete()
|
||||||
|
|
||||||
|
Timber.d("清理缓存资源: 目录=$dirDeleted, Zip=$zipDeleted")
|
||||||
|
dirDeleted || zipDeleted
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "清理缓存资源失败")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化文件大小为可读字符串
|
||||||
|
*/
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,56 +1,19 @@
|
|||||||
//package com.sw.inbound.utils
|
package com.sw.inbound.utils
|
||||||
//
|
|
||||||
//import androidx.compose.foundation.background
|
import android.content.Context
|
||||||
//import androidx.compose.foundation.layout.Box
|
import android.widget.Toast
|
||||||
//import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
//import androidx.compose.foundation.layout.padding
|
/**
|
||||||
//import androidx.compose.foundation.shape.RoundedCornerShape
|
* Toast 扩展方法
|
||||||
//import androidx.compose.material3.Text
|
* 在 Context 上添加 toast 扩展函数,用于显示短时 Toast 消息
|
||||||
//import androidx.compose.runtime.Composable
|
*/
|
||||||
//import androidx.compose.runtime.LaunchedEffect
|
fun Context.toast(message: String, duration: Int = Toast.LENGTH_SHORT) {
|
||||||
//import androidx.compose.runtime.getValue
|
Toast.makeText(this, message, duration).show()
|
||||||
//import androidx.compose.runtime.mutableStateOf
|
}
|
||||||
//import androidx.compose.runtime.setValue
|
|
||||||
//import androidx.compose.ui.Alignment
|
/**
|
||||||
//import androidx.compose.ui.Modifier
|
* 显示长时 Toast 消息
|
||||||
//import androidx.compose.ui.graphics.Color
|
*/
|
||||||
//import androidx.compose.ui.unit.dp
|
fun Context.toastLong(message: String) {
|
||||||
//import androidx.compose.ui.unit.sp
|
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||||
//import kotlinx.coroutines.delay
|
}
|
||||||
//
|
|
||||||
//object ToastUtils {
|
|
||||||
// private var show by mutableStateOf(false)
|
|
||||||
// private var message by mutableStateOf("")
|
|
||||||
//
|
|
||||||
// fun showToast(msg: String) {
|
|
||||||
// message = msg
|
|
||||||
// show = true
|
|
||||||
//// Toast.makeText(ContextUtils.getAppContext(), msg, Toast.LENGTH_LONG).show()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Composable
|
|
||||||
// fun ToastComposable() {
|
|
||||||
// if (show) {
|
|
||||||
// LaunchedEffect(Unit) {
|
|
||||||
// delay(2000) // 自动2秒后消失
|
|
||||||
// show = false
|
|
||||||
// }
|
|
||||||
// Box(
|
|
||||||
// modifier = Modifier
|
|
||||||
//// .fillMaxWidth()
|
|
||||||
// .fillMaxSize()
|
|
||||||
// .padding(bottom = 156.dp),
|
|
||||||
// contentAlignment = Alignment.BottomCenter
|
|
||||||
// ) {
|
|
||||||
// Text(
|
|
||||||
// text = message,
|
|
||||||
// modifier = Modifier
|
|
||||||
// .background(Color.Black.copy(alpha = 0.7f), RoundedCornerShape(8.dp))
|
|
||||||
// .padding(horizontal = 24.dp, vertical = 12.dp),
|
|
||||||
// color = Color.White,
|
|
||||||
// fontSize = 24.sp
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.sw.inbound.utils
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zip 文件下载工具类
|
||||||
|
* 支持大文件下载、进度回调、断点续传等功能
|
||||||
|
*/
|
||||||
|
object ZipDownloadUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载进度回调接口
|
||||||
|
*/
|
||||||
|
interface DownloadProgressListener {
|
||||||
|
/**
|
||||||
|
* 下载进度回调
|
||||||
|
* @param currentSize 已下载字节数
|
||||||
|
* @param totalSize 总字节数
|
||||||
|
* @param progress 进度百分比 (0-100)
|
||||||
|
*/
|
||||||
|
fun onProgress(currentSize: Long, totalSize: Long, progress: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载完成回调
|
||||||
|
* @param file 下载完成的文件
|
||||||
|
*/
|
||||||
|
fun onSuccess(file: File)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载失败回调
|
||||||
|
* @param exception 异常信息
|
||||||
|
*/
|
||||||
|
fun onFailure(exception: Exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载 Zip 文件
|
||||||
|
* @param downloadUrl 下载链接
|
||||||
|
* @param targetFile 目标文件路径
|
||||||
|
* @param listener 进度监听器
|
||||||
|
* @param okHttpClient OkHttp 客户端(可选,默认创建新实例)
|
||||||
|
*/
|
||||||
|
fun downloadZip(
|
||||||
|
downloadUrl: String,
|
||||||
|
targetFile: File,
|
||||||
|
listener: DownloadProgressListener? = null,
|
||||||
|
okHttpClient: OkHttpClient? = null
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// 确保目标文件的父目录存在
|
||||||
|
targetFile.parentFile?.mkdirs()
|
||||||
|
|
||||||
|
val client = okHttpClient ?: OkHttpClient()
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(downloadUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw IOException("下载失败,HTTP 状态码: ${response.code}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseBody = response.body
|
||||||
|
?: throw IOException("响应体为空")
|
||||||
|
|
||||||
|
val totalSize = responseBody.contentLength()
|
||||||
|
Timber.d("开始下载 Zip 文件,总大小: ${formatFileSize(totalSize)}")
|
||||||
|
|
||||||
|
var currentSize = 0L
|
||||||
|
val buffer = ByteArray(8192) // 8KB 缓冲区
|
||||||
|
var bytesRead: Int
|
||||||
|
|
||||||
|
FileOutputStream(targetFile).use { fileOutput ->
|
||||||
|
responseBody.byteStream().use { inputStream ->
|
||||||
|
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||||
|
fileOutput.write(buffer, 0, bytesRead)
|
||||||
|
currentSize += bytesRead
|
||||||
|
|
||||||
|
// 计算进度百分比
|
||||||
|
val progress = if (totalSize > 0) {
|
||||||
|
((currentSize * 100) / totalSize).toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
listener?.onProgress(currentSize, totalSize, progress)
|
||||||
|
Timber.d("下载进度: $progress% (${formatFileSize(currentSize)}/${formatFileSize(totalSize)})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("Zip 文件下载完成: ${targetFile.absolutePath}")
|
||||||
|
listener?.onSuccess(targetFile)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "下载 Zip 文件失败")
|
||||||
|
targetFile.delete() // 删除不完整的文件
|
||||||
|
listener?.onFailure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化文件大小为可读字符串
|
||||||
|
* @param bytes 字节数
|
||||||
|
* @return 格式化后的字符串(如 "10.5 MB")
|
||||||
|
*/
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.sw.inbound.utils
|
||||||
|
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zip 文件解压工具类
|
||||||
|
* 支持解压进度回调、自动创建目录等功能
|
||||||
|
*/
|
||||||
|
object ZipExtractUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压进度回调接口
|
||||||
|
*/
|
||||||
|
interface ExtractProgressListener {
|
||||||
|
/**
|
||||||
|
* 解压进度回调
|
||||||
|
* @param currentFile 当前正在解压的文件名
|
||||||
|
* @param currentIndex 当前解压文件索引
|
||||||
|
* @param totalFiles 总文件数
|
||||||
|
* @param progress 进度百分比 (0-100)
|
||||||
|
*/
|
||||||
|
fun onProgress(currentFile: String, currentIndex: Int, totalFiles: Int, progress: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压完成回调
|
||||||
|
* @param targetDir 解压目标目录
|
||||||
|
*/
|
||||||
|
fun onSuccess(targetDir: File)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压失败回调
|
||||||
|
* @param exception 异常信息
|
||||||
|
*/
|
||||||
|
fun onFailure(exception: Exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压 Zip 文件
|
||||||
|
* @param zipFile 待解压的 Zip 文件
|
||||||
|
* @param targetDir 解压目标目录
|
||||||
|
* @param listener 进度监听器
|
||||||
|
*/
|
||||||
|
fun extractZip(
|
||||||
|
zipFile: File,
|
||||||
|
targetDir: File,
|
||||||
|
listener: ExtractProgressListener? = null
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (!zipFile.exists() || !zipFile.isFile) {
|
||||||
|
throw IllegalArgumentException("Zip 文件不存在或不是文件: ${zipFile.absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保目标目录存在
|
||||||
|
targetDir.mkdirs()
|
||||||
|
|
||||||
|
Timber.d("开始解压 Zip 文件: ${zipFile.absolutePath}")
|
||||||
|
Timber.d("解压目标目录: ${targetDir.absolutePath}")
|
||||||
|
|
||||||
|
// 第一次遍历:计算总文件数
|
||||||
|
val totalFiles = countZipEntries(zipFile)
|
||||||
|
Timber.d("Zip 文件总条目数: $totalFiles")
|
||||||
|
|
||||||
|
var currentIndex = 0
|
||||||
|
|
||||||
|
ZipInputStream(zipFile.inputStream()).use { zipInputStream ->
|
||||||
|
var entry: ZipEntry? = zipInputStream.nextEntry
|
||||||
|
|
||||||
|
while (entry != null) {
|
||||||
|
currentIndex++
|
||||||
|
val entryName = entry.name
|
||||||
|
val progress = ((currentIndex * 100) / totalFiles).coerceIn(0, 100)
|
||||||
|
|
||||||
|
Timber.d("解压进度: $progress% ($currentIndex/$totalFiles) - $entryName")
|
||||||
|
listener?.onProgress(entryName, currentIndex, totalFiles, progress)
|
||||||
|
|
||||||
|
if (entry.isDirectory) {
|
||||||
|
// 创建目录
|
||||||
|
val dir = File(targetDir, entryName)
|
||||||
|
dir.mkdirs()
|
||||||
|
} else {
|
||||||
|
// 解压文件
|
||||||
|
val file = File(targetDir, entryName)
|
||||||
|
file.parentFile?.mkdirs()
|
||||||
|
|
||||||
|
FileOutputStream(file).use { fileOutput ->
|
||||||
|
val buffer = ByteArray(8192) // 8KB 缓冲区
|
||||||
|
var bytesRead: Int
|
||||||
|
while (zipInputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||||
|
fileOutput.write(buffer, 0, bytesRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zipInputStream.closeEntry()
|
||||||
|
entry = zipInputStream.nextEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("Zip 文件解压完成: ${targetDir.absolutePath}")
|
||||||
|
listener?.onSuccess(targetDir)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "解压 Zip 文件失败")
|
||||||
|
listener?.onFailure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算 Zip 文件中的条目总数
|
||||||
|
* @param zipFile Zip 文件
|
||||||
|
* @return 条目总数
|
||||||
|
*/
|
||||||
|
private fun countZipEntries(zipFile: File): Int {
|
||||||
|
var count = 0
|
||||||
|
try {
|
||||||
|
ZipInputStream(zipFile.inputStream()).use { zipInputStream ->
|
||||||
|
while (zipInputStream.nextEntry != null) {
|
||||||
|
count++
|
||||||
|
zipInputStream.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "计算 Zip 条目数失败")
|
||||||
|
}
|
||||||
|
return count.coerceAtLeast(1) // 至少返回 1,避免除以 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package com.sw.inbound.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zip 文件下载和解压工具使用示例
|
||||||
|
*
|
||||||
|
* ============================================
|
||||||
|
* 方案一:使用高层管理器(推荐)
|
||||||
|
* ============================================
|
||||||
|
*
|
||||||
|
* 在 Activity 或 ViewModel 中使用:
|
||||||
|
*
|
||||||
|
* ```kotlin
|
||||||
|
* // 下载并解压模型资源
|
||||||
|
* ModelResourceManager.downloadAndExtractResources(
|
||||||
|
* context = this,
|
||||||
|
* downloadUrl = "https://example.com/model_resources.zip",
|
||||||
|
* callback = object : ModelResourceManager.ResourceCallback {
|
||||||
|
* override fun onDownloadProgress(progress: Int, message: String) {
|
||||||
|
* // 更新下载进度 UI
|
||||||
|
* runOnUiThread {
|
||||||
|
* tvProgress.text = "$progress% - $message"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onExtractProgress(progress: Int, message: String) {
|
||||||
|
* // 更新解压进度 UI
|
||||||
|
* runOnUiThread {
|
||||||
|
* tvProgress.text = "$progress% - $message"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onSuccess(resourceDir: File) {
|
||||||
|
* // 资源解压完成,可以开始使用
|
||||||
|
* Timber.d("资源已准备好: ${resourceDir.absolutePath}")
|
||||||
|
* // 例如:加载 data 文件夹和 .pt 文件
|
||||||
|
* val dataDir = File(resourceDir, "data")
|
||||||
|
* val modelFile = File(resourceDir, "model.pt")
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onFailure(errorMessage: String) {
|
||||||
|
* // 处理错误
|
||||||
|
* runOnUiThread {
|
||||||
|
* ToastUtils.show("资源下载失败: $errorMessage")
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* // 获取已缓存的资源目录
|
||||||
|
* val cachedDir = ModelResourceManager.getCachedResourcesDir(this)
|
||||||
|
* if (cachedDir != null) {
|
||||||
|
* // 资源已缓存,直接使用
|
||||||
|
* val dataDir = File(cachedDir, "data")
|
||||||
|
* val modelFile = File(cachedDir, "model.pt")
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // 清理缓存的资源
|
||||||
|
* ModelResourceManager.clearCachedResources(this)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ============================================
|
||||||
|
* 方案二:分别使用下载和解压工具
|
||||||
|
* ============================================
|
||||||
|
*
|
||||||
|
* 如果需要更细粒度的控制,可以分别使用:
|
||||||
|
*
|
||||||
|
* ```kotlin
|
||||||
|
* // 1. 下载 Zip 文件
|
||||||
|
* val zipFile = File(context.cacheDir, "model_resources.zip")
|
||||||
|
* ZipDownloadUtils.downloadZip(
|
||||||
|
* downloadUrl = "https://example.com/model_resources.zip",
|
||||||
|
* targetFile = zipFile,
|
||||||
|
* listener = object : ZipDownloadUtils.DownloadProgressListener {
|
||||||
|
* override fun onProgress(currentSize: Long, totalSize: Long, progress: Int) {
|
||||||
|
* Timber.d("下载进度: $progress%")
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onSuccess(file: File) {
|
||||||
|
* Timber.d("下载完成: ${file.absolutePath}")
|
||||||
|
* // 继续解压
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onFailure(exception: Exception) {
|
||||||
|
* Timber.e(exception, "下载失败")
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* // 2. 解压 Zip 文件
|
||||||
|
* val targetDir = File(context.cacheDir, "model_resources")
|
||||||
|
* ZipExtractUtils.extractZip(
|
||||||
|
* zipFile = zipFile,
|
||||||
|
* targetDir = targetDir,
|
||||||
|
* listener = object : ZipExtractUtils.ExtractProgressListener {
|
||||||
|
* override fun onProgress(
|
||||||
|
* currentFile: String,
|
||||||
|
* currentIndex: Int,
|
||||||
|
* totalFiles: Int,
|
||||||
|
* progress: Int
|
||||||
|
* ) {
|
||||||
|
* Timber.d("解压进度: $progress% - $currentFile")
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onSuccess(targetDir: File) {
|
||||||
|
* Timber.d("解压完成: ${targetDir.absolutePath}")
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onFailure(exception: Exception) {
|
||||||
|
* Timber.e(exception, "解压失败")
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ============================================
|
||||||
|
* 关键特性
|
||||||
|
* ============================================
|
||||||
|
*
|
||||||
|
* 1. 下载特性:
|
||||||
|
* - 支持大文件下载(120MB+ 无压力)
|
||||||
|
* - 实时进度回调
|
||||||
|
* - 自动错误处理和清理
|
||||||
|
* - 使用 8KB 缓冲区,内存占用低
|
||||||
|
*
|
||||||
|
* 2. 解压特性:
|
||||||
|
* - 支持大型 Zip 文件
|
||||||
|
* - 自动创建目录结构
|
||||||
|
* - 实时进度回调
|
||||||
|
* - 使用 Java 内置 ZipInputStream,无额外依赖
|
||||||
|
*
|
||||||
|
* 3. 资源管理:
|
||||||
|
* - 自动缓存到应用缓存目录
|
||||||
|
* - 支持检查缓存是否存在
|
||||||
|
* - 支持清理缓存
|
||||||
|
* - 完整的日志记录
|
||||||
|
*
|
||||||
|
* ============================================
|
||||||
|
* 建议的集成方式
|
||||||
|
* ============================================
|
||||||
|
*
|
||||||
|
* 1. 在 Application 初始化时检查缓存:
|
||||||
|
* ```kotlin
|
||||||
|
* class MyApp : Application() {
|
||||||
|
* override fun onCreate() {
|
||||||
|
* super.onCreate()
|
||||||
|
* // 检查模型资源是否已缓存
|
||||||
|
* val cachedDir = ModelResourceManager.getCachedResourcesDir(this)
|
||||||
|
* if (cachedDir == null) {
|
||||||
|
* // 需要下载资源,可以在首次启动时提示用户
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* 2. 在需要使用资源的 Activity/Fragment 中:
|
||||||
|
* ```kotlin
|
||||||
|
* val cachedDir = ModelResourceManager.getCachedResourcesDir(context)
|
||||||
|
* if (cachedDir != null) {
|
||||||
|
* // 资源已准备好,直接使用
|
||||||
|
* useModelResources(cachedDir)
|
||||||
|
* } else {
|
||||||
|
* // 资源不存在,触发下载
|
||||||
|
* downloadResources()
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* 3. 在后台线程中执行下载和解压:
|
||||||
|
* ```kotlin
|
||||||
|
* Thread {
|
||||||
|
* ModelResourceManager.downloadAndExtractResources(
|
||||||
|
* context = this,
|
||||||
|
* downloadUrl = resourceUrl,
|
||||||
|
* callback = callback
|
||||||
|
* )
|
||||||
|
* }.start()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ============================================
|
||||||
|
* 注意事项
|
||||||
|
* ============================================
|
||||||
|
*
|
||||||
|
* 1. 下载和解压操作会阻塞线程,建议在后台线程执行
|
||||||
|
* 2. 确保应用有网络权限和文件读写权限
|
||||||
|
* 3. 缓存目录会占用应用缓存空间,大文件需要足够的存储空间
|
||||||
|
* 4. 下载失败时会自动删除不完整的文件
|
||||||
|
* 5. 所有操作都有详细的日志记录,可通过 Timber 查看
|
||||||
|
*/
|
||||||
|
object ZipToolsUsageExample
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@mipmap/bg"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- 标题栏区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingStart="60dp"
|
||||||
|
android:paddingEnd="60dp"
|
||||||
|
android:paddingTop="22dp">
|
||||||
|
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivBack"
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:contentDescription="返回"
|
||||||
|
android:src="@mipmap/ic_back" />
|
||||||
|
|
||||||
|
<!-- 标题文字 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:text="趣味页面"
|
||||||
|
android:textColor="@color/title"
|
||||||
|
android:textSize="36sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<!-- 占位弹性空间 -->
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="60dp">
|
||||||
|
|
||||||
|
<!-- 下载按钮 -->
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnDownload"
|
||||||
|
android:layout_width="400dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:text="1. 下载 Zip 文件"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:backgroundTint="@color/title"
|
||||||
|
android:layout_marginBottom="30dp" />
|
||||||
|
|
||||||
|
<!-- 解压按钮 -->
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnExtract"
|
||||||
|
android:layout_width="400dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:text="2. 解压 Zip 文件"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:backgroundTint="@color/title"
|
||||||
|
android:layout_marginBottom="30dp" />
|
||||||
|
|
||||||
|
<!-- 读取并保存按钮 -->
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnLoadAndSave"
|
||||||
|
android:layout_width="400dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:text="3. 读取并保存到数据库"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:backgroundTint="@color/title"
|
||||||
|
android:layout_marginBottom="30dp" />
|
||||||
|
|
||||||
|
<!-- 进度/状态显示 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvProgress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="就绪"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:layout_marginTop="30dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@mipmap/bg"
|
android:background="@mipmap/bg"
|
||||||
@@ -39,7 +40,8 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/black"
|
android:textColor="@color/black"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:visibility="visible" />
|
android:visibility="visible"
|
||||||
|
tools:text="2026-03-13 星期五"/>
|
||||||
|
|
||||||
<!-- 用户信息区(已登录时显示,未登录隐藏) -->
|
<!-- 用户信息区(已登录时显示,未登录隐藏) -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
Reference in New Issue
Block a user