1、修改下载器bug
This commit is contained in:
shenzuqiang 2026-03-16 15:42:47 +08:00
parent cbcfc0c315
commit 9bdb30e28f
6 changed files with 438 additions and 120 deletions

View File

@ -43,7 +43,6 @@ import com.google.accompanist.permissions.shouldShowRationale
import com.img.rabbit.R
import com.img.rabbit.components.CenterToast
import com.img.rabbit.provider.utils.NetworkUtils.globalNetworkStatus
import kotlinx.coroutines.CoroutineScope
@SuppressLint("UnrememberedMutableState")
@OptIn(ExperimentalPermissionsApi::class)

View File

@ -1,92 +1,384 @@
package com.img.rabbit.utils
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import okio.Buffer
import okio.BufferedSource
import okio.ForwardingSource
import okio.buffer
import okio.sink
import java.io.File
import java.io.IOException
import android.os.Handler
import android.os.Looper
import android.util.Log
import okhttp3.*
import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.io.*
import java.net.URL
import java.util.concurrent.CancellationException
import java.util.concurrent.TimeUnit
object FileDownloadUtil {
private val client = OkHttpClient.Builder().build()
fun download(url: String, saveFile: File, listener: DownloadListener) {
val request = Request.Builder().url(url).build()
class DownUtils private constructor() {
// 拦截器处理进度
val progressClient = client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body, listener))
.build()
}
companion object {
private const val TAG = "DownLoadUtils"
private val downLoadHttpUtils: DownUtils by lazy {
DownUtils()
}
@JvmStatic
@Synchronized
fun getInstance(): DownUtils {
return downLoadHttpUtils
}
}
private var downloadSizeInfo = mutableMapOf<String, Long>()
private var cancelledList = mutableListOf<String>()
private var buffSize = 4096//建议设置为2048
fun setBuffSize(size: Int): DownUtils {
this.buffSize = size
return this
}
private var interceptor: Interceptor? = null
fun setInterceptor(interceptor: Interceptor?): DownUtils {
this.interceptor = interceptor
return this
}
private var readTimeOut = 30L
fun setReadTImeOut(read: Long): DownUtils {
this.readTimeOut = read
return this
}
private var writeTimeout = 30L
fun setWriteTimeOut(write: Long): DownUtils {
this.writeTimeout = write
return this
}
private var connectTimeout = 30L
fun setConnectTimeOut(connect: Long): DownUtils {
this.connectTimeout = connect
return this
}
var filePath = ""
fun setFilePath(path: String): DownUtils {
this.filePath = path
return this
}
private var fileName = ""
fun setFileName(name: String): DownUtils {
this.fileName = name
return this
}
private var deleteWhenException = true
fun setDeleteWhenException(dele: Boolean): DownUtils {
this.deleteWhenException = dele
return this
}
private val requestBuilder: Request.Builder = Request.Builder()
private var urlBuilder: HttpUrl.Builder? = null
private val okHttpClient = lazy {
OkHttpClient.Builder()
.readTimeout(readTimeOut, TimeUnit.SECONDS)
.writeTimeout(writeTimeout, TimeUnit.SECONDS)
.connectTimeout(connectTimeout, TimeUnit.SECONDS)
.addInterceptor(interceptor ?: LoggingInterceptor())
.build()
}
private var actionGetTotal: (total: Long) -> Unit? = { _ -> }
private var actionProgress: (position: Long) -> Unit? = { _ -> }
private var actionSuccess: (file: File) -> Unit? = { _ -> }
private var actionCancel: () -> Unit? = { }
private var actionFail: (msg: String) -> Unit? = { _ -> }
progressClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
listener.onError(e)
fun setActionCallBack(
actionGetTotal: (total: Long) -> Unit,
actionProgress: (position: Long) -> Unit,
actionSuccess: (file: File) -> Unit,
actionFail: (msg: String) -> Unit,
): DownUtils {
this.actionGetTotal = actionGetTotal
this.actionProgress = actionProgress
this.actionSuccess = actionSuccess
this.actionFail = actionFail
return this
}
private var downCallBack: DownCallBack? = null
fun setDownCallBack(callBack: DownCallBack): DownUtils {
this.downCallBack = callBack
return this
}
fun initUrl(url: String, params: Map<String, String>?): DownUtils {
urlBuilder = url.toHttpUrlOrNull()?.newBuilder()
if (params.isNullOrEmpty()) {
return this
} else {
for ((k, v) in params) {
checkName(k)
urlBuilder?.setQueryParameter(k, v)
}
}
return this
}
fun addHeader(map: Map<String, String>): DownUtils {
requestBuilder.headers(map.toHeaders())
return this
}
private fun checkName(name: String) {
require(name.isNotEmpty()) { "name is empty" }
}
fun down() {
if (urlBuilder == null) {
throw IllegalStateException("url not init")
} else {
doDown()
}
}
private fun doDown() {
val startTime = System.currentTimeMillis()
Log.i(TAG, "startTime=$startTime")
val url = urlBuilder?.build()
if (url == null) {
doException("url is null")
return
}
if (isDowning(filePath + fileName)) {
return
}
cancelledList.remove(fileName)
val currentLen = downloadSizeInfo[fileName] ?: 0L
if (isCanContinueDownload(url.toUrl(), currentLen)) {
requestBuilder.removeHeader("RANGE")
requestBuilder.addHeader("RANGE", "bytes=${currentLen}-")
}
val request = requestBuilder.url(url).tag(filePath + fileName).build()
var `is`: InputStream? = null
var raf: RandomAccessFile? = null
var file: File? = null
try {
val response = okHttpClient.value.newCall(request).execute()
val total = response.body.contentLength()
doGetTotal(currentLen + total)
val buf = ByteArray(buffSize)
var len: Int
file = if (fileName.isEmpty()) {
File(filePath)
} else {
val fileDir = File(filePath)
if (!fileDir.exists() || !fileDir.isDirectory) {
fileDir.mkdirs()
}
File(filePath, fileName)
}
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
listener.onError(IOException("Unexpected code $response"))
return
}
`is` = response.body.byteStream()
raf = RandomAccessFile(file, "rw")
raf.seek(currentLen)
try {
val totalSize = response.body.contentLength()
listener.onStart(totalSize)
var sum: Long = currentLen
while (`is`.read(buf).also { len = it } != -1) {
if (isCancelled()) throw CancellationException()
raf.write(buf, 0, len)
sum += len.toLong()
downloadSizeInfo[fileName] = sum
doProgress(sum)
}
Log.e(TAG, "download success")
response.body.source().use { source ->
saveFile.sink().buffer().use { sink ->
sink.writeAll(source)
}
}
listener.onSuccess(saveFile)
} catch (e: Exception) {
listener.onError(e)
if (!file.exists()) {
throw FileNotFoundException("file create err,not exists")
} else {
doSuccess(file)
}
Log.e(TAG, "totalTime=" + (System.currentTimeMillis() - startTime))
} catch (e: Exception) {
if (deleteWhenException && file?.exists() == true) file.delete()
Log.e(TAG, "download failed : " + e.message)
doException(e.message.toString())
} finally {
try {
`is`?.close()
} catch (e: IOException) {
e.printStackTrace()
}
try {
raf?.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
private fun isCanContinueDownload(url: URL, start: Long): Boolean { //是否支持断点下载
val requestBuilder = Request.Builder()
requestBuilder.addHeader("RANGE", "bytes=$start-")
requestBuilder.addHeader("Connection", "close")
val request: Request = requestBuilder.url(url).head().build()
val response: Response = okHttpClient.value.newCall(request).execute()
return if (response.isSuccessful) {
if (response.code == 206) { //支持
response.close()
true
} else { //不支持
response.close()
false
}
} else {
response.close()
false
}
}
private fun isDowning(tag: String): Boolean {
for (call in okHttpClient.value.dispatcher.runningCalls()) {
if (call.request().tag() == tag) {
return true
}
}
return false
}
private fun isCancelled(): Boolean {
return cancelledList.contains(fileName)
}
fun cancel() {
cancel(filePath + fileName)
}
fun cancel(tag: String) {
cancelledList.add(fileName)
if (okHttpClient.value.dispatcher.runningCalls().isNotEmpty()) {
for (call in okHttpClient.value.dispatcher.runningCalls()) {
if (call.request().tag() == tag) {
call.cancel()
}
}
})
}
doCancel()
}
}
// 进度回调接口
interface DownloadListener {
fun onStart(totalSize: Long) // 开始下载,返回文件总大小
fun onProgress(progress: Int, current: Long) // 进度(0-100),当前已下载字节
fun onSuccess(file: File) // 下载成功
fun onError(e: Exception) // 下载失败
}
// 自定义 ResponseBody 用于拦截进度
class ProgressResponseBody(
private val responseBody: ResponseBody,
private val listener: DownloadListener
) : ResponseBody() {
override fun contentType() = responseBody.contentType()
override fun contentLength() = responseBody.contentLength()
override fun source(): BufferedSource {
return object : ForwardingSource(responseBody.source()) {
var totalBytesRead = 0L
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
val progress = if (contentLength() > 0) (totalBytesRead * 100 / contentLength()).toInt() else 0
// 回调进度
listener.onProgress(progress, totalBytesRead)
return bytesRead
private fun doException(err: String) {
runOnUiThread {
if (downCallBack == null) {
actionFail.invoke(err)
} else {
downCallBack?.fail(err)
}
}.buffer()
mainThread = null
}
}
private fun doSuccess(file: File?) {
runOnUiThread {
if (file == null) {
doException("file not exit")
} else {
if (downCallBack == null) {
actionSuccess.invoke(file)
} else {
downCallBack?.success(file)
}
downloadSizeInfo.remove(fileName)
}
mainThread = null
}
}
private fun doGetTotal(total: Long) {
runOnUiThread {
if (downCallBack == null) {
actionGetTotal.invoke(total)
} else {
downCallBack?.total(total)
}
}
mainThread = null
}
private fun doProgress(progress: Long) {
runOnUiThread {
if (downCallBack == null) {
actionProgress.invoke(progress)
} else {
downCallBack?.progress(progress)
}
}
}
private fun doCancel() {
runOnUiThread {
if (downCallBack == null) {
actionCancel.invoke()
} else {
downCallBack?.cancel()
}
}
mainThread = null
}
private var mainThread: Handler? = null
private fun runOnUiThread(action: () -> Unit) {
if (Looper.myLooper() != Looper.getMainLooper()) {
if (mainThread == null) {
mainThread = Handler(Looper.getMainLooper())
}
mainThread?.post {
action.invoke()
}
return
}
action.invoke()
}
class LoggingInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val startTime = System.nanoTime()
Log.d(
TAG, String.format(
"Sending request %s on %s%n%s",
request.url, chain.connection(), request.headers
)
)
val response: Response = chain.proceed(request)
val endTime = System.nanoTime()
Log.d(
TAG, String.format(
"Received response for %s in %.1fms%n%s",
response.request.url, (endTime - startTime) / 1e6, response.headers
)
)
return response
}
companion object {
private const val TAG = "LoggingInterceptor"
}
}
interface DownCallBack {
fun success(file: File)
fun fail(str: String)
fun progress(position: Long)
fun total(total: Long)
fun cancel()
}
}

View File

@ -152,10 +152,8 @@ object UniAppUtils {
private fun startUniMp(context: Context, uniVersion: UniVersionEntity, onResult:(loading: Boolean) -> Unit){
val uniMp = _uniMpFlow.value[uniVersion.unimp_id]//uniMpPair[uniVersion.unimp_id]
if(uniMp?.isRuning == true){
Log.i(TAG, "startUniMp: 运行中...")
uniMp.showUniMP()
}else{
Log.i(TAG, "startUniMp: 重新加载...")
val configuration = getUniMPOpenConfiguration()
if(uniVersion.unimp_type == "wx"){
configuration.splashClass = UniMPWxSplashView::class.java
@ -278,7 +276,6 @@ object UniAppUtils {
}
}
}else if(uniState == UniMpUpdate.DOWNLOAD_FAIL){
Log.i(TAG, "下载wgt失败...")
onProgress(UniMpUpdate.DOWNLOAD_FAIL, -1f)
}
}
@ -307,16 +304,13 @@ object UniAppUtils {
oldFile.delete()
}
onProgress(UniMpUpdate.DOWNLOAD_LOADING, null, 0.001f)
onProgress(UniMpUpdate.DOWNLOAD_LOADING, null, 0.0f)
scope.launch {
val isAvailable = isFileDownloadable(uniVersion.url)
if(!isAvailable){
Log.i(TAG, "下载失败,无效地址 ------>${uniVersion.url}")
onProgress(UniMpUpdate.DOWNLOAD_FAIL, null, -1f)
CenterToast.show("下载失败...")
}else{
Log.i(TAG, "下载wgt---->downloadUniMp: $path/$wgtName ------>${uniVersion.url}")
UpdateUtils.download(
scope = scope,
url = uniVersion.url,
@ -334,10 +328,8 @@ object UniAppUtils {
onFinish = {isSuccess, filePath ->
if(isSuccess){
PreferenceUtil.saveWgtVersion(uniMpId, uniVersion.version)
Log.i(TAG, "下载完成---->updateUniMp: $filePath")
onProgress(UniMpUpdate.DOWNLOAD_FINISH, filePath, 1f)
}else{
Log.i(TAG, "下载失败---->updateUniMp: $filePath")
onProgress(UniMpUpdate.DOWNLOAD_FAIL, filePath, -1f)
CenterToast.show("下载失败...")
@ -419,7 +411,7 @@ object UniAppUtils {
}
}
} catch (e: Exception) {
Log.i(TAG, "异常:${e.message}")
e.printStackTrace()
}
return@withContext false
}

View File

@ -11,6 +11,9 @@ import kotlinx.coroutines.launch
import net.lingala.zip4j.ZipFile
import java.io.File
/**
* 更新管理器本质下载器包装
*/
object UpdateUtils {
private const val TAG = "UpdateUtils"
@SuppressLint("SetTextI18n")
@ -20,23 +23,51 @@ object UpdateUtils {
if(destination.exists()){
destination.delete()
}
FileDownloadUtil.download(url, destination, object : DownloadListener {
override fun onStart(totalSize: Long) {
onProgress(0)
}
var totalProgress = 0L
DownUtils.getInstance()
.setReadTImeOut(10L)
.setDeleteWhenException(false)
.initUrl(url, null)
.setFilePath(filePath)
.setFileName(fileName)
.setActionCallBack(
{ totalProgress = it },
{
val percent = it.toDouble() / totalProgress.toDouble() * 100
val curProgress = percent.toInt()
onProgress(curProgress)
},
{
onFinish(true, it.absolutePath)
}, {
onFinish(false, null)
}).down()
override fun onProgress(progress: Int, current: Long) {
onProgress(progress)
}
/*
applicationContext?.let {
AutoDownloadManager.startAutoDownload(it, url, destination,object: DownloadListener {
override fun onStart(totalSize: Long) {
onProgress(0)
}
override fun onSuccess(file: File) {
onFinish(true, file.absolutePath)
}
override fun onProgress(
progress: Int,
current: Long,
total: Long
) {
onProgress(progress)
}
override fun onError(e: Exception) {
onFinish(false, null)
}
})
override fun onSuccess(file: File) {
onFinish(true, file.absolutePath)
}
override fun onError(e: Exception) {
onFinish(false, null)
}
})
}
*/
}
}

View File

@ -9,8 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.viewModelScope
import com.img.rabbit.bean.local.ErrorBean
import com.img.rabbit.provider.api.ApiManager
import com.img.rabbit.utils.DownloadListener
import com.img.rabbit.utils.FileDownloadUtil
import com.img.rabbit.utils.DownUtils
import com.img.rabbit.utils.FileUtils
import com.img.rabbit.utils.ImageUtils
import com.img.rabbit.utils.PhotoCutter
@ -104,25 +103,34 @@ class CutoutViewModel : BaseViewModel() {
targetFile.delete()
}
viewModelScope.launch(Dispatchers.IO) {
FileDownloadUtil.download(url, targetFile, object : DownloadListener {
override fun onStart(totalSize: Long) {
}
DownUtils.getInstance()
.initUrl(url, null)
.setFilePath(targetPath)
.setFileName(targetFileName)
.setDownCallBack(object : DownUtils.DownCallBack {
override fun success(file: File) {
// 3. 下载成功后,将文件解析为 Bitmap 并回调给 UI
val bitmap = android.graphics.BitmapFactory.decodeFile(file.absolutePath)
onResult(true, bitmap)
}
override fun onProgress(progress: Int, current: Long) {
// 如果需要进度可以在这里处理
}
override fun fail(str: String) {
onResult(false, null)
}
override fun onSuccess(file: File) {
// 3. 下载成功后,将文件解析为 Bitmap 并回调给 UI
val bitmap = android.graphics.BitmapFactory.decodeFile(file.absolutePath)
onResult(true, bitmap)
}
override fun progress(position: Long) {
// 如果需要进度可以在这里处理
}
override fun onError(e: Exception) {
onResult(false, null)
}
})
override fun total(total: Long) {
// 获取总大小
}
override fun cancel() {
onResult(false, null)
}
})
.down()
}
}catch (e: Exception){
e.printStackTrace()

View File

@ -16,8 +16,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.img.rabbit.bean.response.UniVersionEntity
import com.img.rabbit.config.Constants
import com.img.rabbit.provider.api.ApiManager
import com.img.rabbit.provider.storage.PreferenceUtil
@ -27,8 +25,6 @@ import com.tencent.mm.opensdk.constants.ConstantsAPI
import com.tencent.mm.opensdk.openapi.IWXAPI
import com.tencent.mm.opensdk.openapi.WXAPIFactory
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
@SuppressLint("ObsoleteSdkInt")
class GeneralViewModel: BaseViewModel(){