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.R
import com.img.rabbit.components.CenterToast import com.img.rabbit.components.CenterToast
import com.img.rabbit.provider.utils.NetworkUtils.globalNetworkStatus import com.img.rabbit.provider.utils.NetworkUtils.globalNetworkStatus
import kotlinx.coroutines.CoroutineScope
@SuppressLint("UnrememberedMutableState") @SuppressLint("UnrememberedMutableState")
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)

View File

@ -1,92 +1,384 @@
package com.img.rabbit.utils package com.img.rabbit.utils
import okhttp3.Call import android.os.Handler
import okhttp3.Callback import android.os.Looper
import okhttp3.OkHttpClient import android.util.Log
import okhttp3.Request import okhttp3.*
import okhttp3.Response import okhttp3.Headers.Companion.toHeaders
import okhttp3.ResponseBody import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okio.Buffer import java.io.*
import okio.BufferedSource import java.net.URL
import okio.ForwardingSource import java.util.concurrent.CancellationException
import okio.buffer import java.util.concurrent.TimeUnit
import okio.sink
import java.io.File
import java.io.IOException
object FileDownloadUtil {
private val client = OkHttpClient.Builder().build()
fun download(url: String, saveFile: File, listener: DownloadListener) { class DownUtils private constructor() {
val request = Request.Builder().url(url).build()
// 拦截器处理进度 companion object {
val progressClient = client.newBuilder() private const val TAG = "DownLoadUtils"
.addNetworkInterceptor { chain -> private val downLoadHttpUtils: DownUtils by lazy {
val originalResponse = chain.proceed(chain.request()) DownUtils()
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body, listener))
.build()
}
.build()
progressClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
listener.onError(e)
} }
override fun onResponse(call: Call, response: Response) { @JvmStatic
if (!response.isSuccessful) { @Synchronized
listener.onError(IOException("Unexpected code $response")) 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? = { _ -> }
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 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 { try {
val totalSize = response.body.contentLength() val response = okHttpClient.value.newCall(request).execute()
listener.onStart(totalSize) val total = response.body.contentLength()
doGetTotal(currentLen + total)
response.body.source().use { source -> val buf = ByteArray(buffSize)
saveFile.sink().buffer().use { sink -> var len: Int
sink.writeAll(source)
file = if (fileName.isEmpty()) {
File(filePath)
} else {
val fileDir = File(filePath)
if (!fileDir.exists() || !fileDir.isDirectory) {
fileDir.mkdirs()
} }
File(filePath, fileName)
} }
listener.onSuccess(saveFile)
`is` = response.body.byteStream()
raf = RandomAccessFile(file, "rw")
raf.seek(currentLen)
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")
if (!file.exists()) {
throw FileNotFoundException("file create err,not exists")
} else {
doSuccess(file)
}
Log.e(TAG, "totalTime=" + (System.currentTimeMillis() - startTime))
} catch (e: Exception) { } catch (e: Exception) {
listener.onError(e) 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()
}
private fun doException(err: String) {
runOnUiThread {
if (downCallBack == null) {
actionFail.invoke(err)
} else {
downCallBack?.fail(err)
}
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()
} }
} }
// 进度回调接口
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
}
}.buffer()
}
}

View File

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

View File

@ -11,6 +11,9 @@ import kotlinx.coroutines.launch
import net.lingala.zip4j.ZipFile import net.lingala.zip4j.ZipFile
import java.io.File import java.io.File
/**
* 更新管理器本质下载器包装
*/
object UpdateUtils { object UpdateUtils {
private const val TAG = "UpdateUtils" private const val TAG = "UpdateUtils"
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@ -20,12 +23,38 @@ object UpdateUtils {
if(destination.exists()){ if(destination.exists()){
destination.delete() destination.delete()
} }
FileDownloadUtil.download(url, destination, object : DownloadListener { 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()
/*
applicationContext?.let {
AutoDownloadManager.startAutoDownload(it, url, destination,object: DownloadListener {
override fun onStart(totalSize: Long) { override fun onStart(totalSize: Long) {
onProgress(0) onProgress(0)
} }
override fun onProgress(progress: Int, current: Long) { override fun onProgress(
progress: Int,
current: Long,
total: Long
) {
onProgress(progress) onProgress(progress)
} }
@ -38,6 +67,8 @@ object UpdateUtils {
} }
}) })
} }
*/
}
} }
fun install(context: Context, apkFilePath: String) { fun install(context: Context, apkFilePath: String) {

View File

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

View File

@ -16,8 +16,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.navigation.NavHostController 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.config.Constants
import com.img.rabbit.provider.api.ApiManager import com.img.rabbit.provider.api.ApiManager
import com.img.rabbit.provider.storage.PreferenceUtil 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.IWXAPI
import com.tencent.mm.opensdk.openapi.WXAPIFactory import com.tencent.mm.opensdk.openapi.WXAPIFactory
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
@SuppressLint("ObsoleteSdkInt") @SuppressLint("ObsoleteSdkInt")
class GeneralViewModel: BaseViewModel(){ class GeneralViewModel: BaseViewModel(){