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,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
|
||||
//
|
||||
//import androidx.compose.foundation.background
|
||||
//import androidx.compose.foundation.layout.Box
|
||||
//import androidx.compose.foundation.layout.fillMaxSize
|
||||
//import androidx.compose.foundation.layout.padding
|
||||
//import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
//import androidx.compose.material3.Text
|
||||
//import androidx.compose.runtime.Composable
|
||||
//import androidx.compose.runtime.LaunchedEffect
|
||||
//import androidx.compose.runtime.getValue
|
||||
//import androidx.compose.runtime.mutableStateOf
|
||||
//import androidx.compose.runtime.setValue
|
||||
//import androidx.compose.ui.Alignment
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.graphics.Color
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import androidx.compose.ui.unit.sp
|
||||
//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
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
|
||||
/**
|
||||
* Toast 扩展方法
|
||||
* 在 Context 上添加 toast 扩展函数,用于显示短时 Toast 消息
|
||||
*/
|
||||
fun Context.toast(message: String, duration: Int = Toast.LENGTH_SHORT) {
|
||||
Toast.makeText(this, message, duration).show()
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示长时 Toast 消息
|
||||
*/
|
||||
fun Context.toastLong(message: String) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user