diff --git a/app/src/main/java/com/img/rabbit/MainActivity.kt b/app/src/main/java/com/img/rabbit/MainActivity.kt index 6b93b40..914a0fb 100644 --- a/app/src/main/java/com/img/rabbit/MainActivity.kt +++ b/app/src/main/java/com/img/rabbit/MainActivity.kt @@ -245,7 +245,6 @@ class MainActivity : ComponentActivity(), LoadingCallback { //资源下载进度 if (progress != null) { progressWGTToPageState.floatValue = progress - Log.i("HomeScreen", "DOWNLOAD_PROGRESS:$progress") } } diff --git a/app/src/main/java/com/img/rabbit/pages/dialog/TipsUniMpDialog.kt b/app/src/main/java/com/img/rabbit/pages/dialog/TipsUniMpDialog.kt index 304e62d..8437c26 100644 --- a/app/src/main/java/com/img/rabbit/pages/dialog/TipsUniMpDialog.kt +++ b/app/src/main/java/com/img/rabbit/pages/dialog/TipsUniMpDialog.kt @@ -235,7 +235,6 @@ fun TipsUniMpDialog( //资源下载进度 if(progress != null){ downProgress.value = progress - Log.i("HomeScreen","DOWNLOAD_PROGRESS:$progress") } } diff --git a/app/src/main/java/com/img/rabbit/utils/DownUtils.kt b/app/src/main/java/com/img/rabbit/utils/DownUtils.kt index cf3fcb6..83189d4 100644 --- a/app/src/main/java/com/img/rabbit/utils/DownUtils.kt +++ b/app/src/main/java/com/img/rabbit/utils/DownUtils.kt @@ -4,84 +4,47 @@ 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.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit - class DownUtils private constructor() { companion object { - private const val TAG = "DownLoadUtils" - private val downLoadHttpUtils: DownUtils by lazy { + private const val TAG = "DownUtils" + private val _instance: DownUtils by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { DownUtils() } @JvmStatic - @Synchronized - fun getInstance(): DownUtils { - return downLoadHttpUtils - } + fun getInstance(): DownUtils = _instance } - private var downloadSizeInfo = mutableMapOf() - private var cancelledList = mutableListOf() + // 1. 线程安全集合:防止并发修改异常 + private val downloadSizeInfo = ConcurrentHashMap() + private val cancelledList = CopyOnWriteArrayList() - 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 - } + // 2. 常驻 Handler:解决 runOnUiThread 随机崩溃 + private val mainHandler = Handler(Looper.getMainLooper()) + private var buffSize = 8192 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 interceptor: Interceptor? = null + // 任务参数 + private var filePath = "" 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 deleteWhenException = false private var urlBuilder: HttpUrl.Builder? = null + private val headers = mutableMapOf() - private val okHttpClient = lazy { + private val okHttpClient: OkHttpClient by lazy { OkHttpClient.Builder() .readTimeout(readTimeOut, TimeUnit.SECONDS) .writeTimeout(writeTimeout, TimeUnit.SECONDS) @@ -89,289 +52,199 @@ class DownUtils private constructor() { .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? = { _ -> } + + 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 = {} + private var downCallBack: DownCallBack? = null + + // --- 配置方法 --- + + fun setBuffSize(size: Int): DownUtils { this.buffSize = size; return this } + fun setFilePath(path: String): DownUtils { this.filePath = path; return this } + fun setFileName(name: String): DownUtils { this.fileName = name; return this } + fun initUrl(url: String, params: Map? = null): DownUtils { + urlBuilder = url.toHttpUrlOrNull()?.newBuilder() + params?.forEach { (k, v) -> urlBuilder?.setQueryParameter(k, v) } + return this + } + fun addHeader(map: Map): DownUtils { this.headers.putAll(map); return this } fun setActionCallBack( - actionGetTotal: (total: Long) -> Unit, - actionProgress: (position: Long) -> Unit, - actionSuccess: (file: File) -> Unit, - actionFail: (msg: String) -> Unit, + getTotal: (Long) -> Unit, + progress: (Long) -> Unit, + success: (File) -> Unit, + fail: (String) -> Unit ): DownUtils { - this.actionGetTotal = actionGetTotal - this.actionProgress = actionProgress - this.actionSuccess = actionSuccess - this.actionFail = actionFail - + this.actionGetTotal = getTotal + this.actionProgress = progress + this.actionSuccess = success + this.actionFail = fail return this } - private var downCallBack: DownCallBack? = null fun setDownCallBack(callBack: DownCallBack): DownUtils { this.downCallBack = callBack return this } - fun initUrl(url: String, params: Map?): 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): 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() - } + val url = urlBuilder?.build() ?: throw IllegalStateException("URL not initialized") + doDown(url) } - 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 - } + private fun doDown(url: HttpUrl) { + val tag = filePath + fileName + if (isDowning(tag)) 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() + // 3. 状态恢复:读取上次进度 + var currentLen = downloadSizeInfo[fileName] ?: 0L - var `is`: InputStream? = null + // 校验:如果上次进度不为0,验证服务器是否支持断点续传 + if (currentLen > 0) { + if (!isCanContinueDownload(url.toUrl(), currentLen)) { + currentLen = 0L // 不支持续传则重置 + downloadSizeInfo[fileName] = 0L + } + } + + val request = Request.Builder() + .url(url) + .apply { + headers.forEach { (k, v) -> addHeader(k, v) } + if (currentLen > 0) addHeader("RANGE", "bytes=$currentLen-") + } + .tag(tag) + .build() + + var inputStream: InputStream? = null var raf: RandomAccessFile? = null var file: File? = null + try { - val response = okHttpClient.value.newCall(request).execute() - val total = response.body.contentLength() + val response = okHttpClient.newCall(request).execute() + if (!response.isSuccessful) throw IOException("Unexpected code $response") + + val body = response.body + val total = body.contentLength() doGetTotal(currentLen + total) + val fileDir = File(filePath) + if (!fileDir.exists()) fileDir.mkdirs() + file = File(fileDir, fileName) + + inputStream = body.byteStream() + raf = RandomAccessFile(file, "rw") + + // 4. 只有响应码为 206 时才 seek,否则强制重置到文件开头 + if (response.code == 206) { + raf.seek(currentLen) + } else { + raf.seek(0) + currentLen = 0 + } + val buf = ByteArray(buffSize) var len: Int + var sum = currentLen + var lastUpdateTime = 0L - file = if (fileName.isEmpty()) { - File(filePath) - } else { - val fileDir = File(filePath) - if (!fileDir.exists() || !fileDir.isDirectory) { - fileDir.mkdirs() - } - File(filePath, fileName) - } - - `is` = response.body.byteStream() - raf = RandomAccessFile(file, "rw") - raf.seek(currentLen) - - var sum: Long = currentLen - while (`is`.read(buf).also { len = it } != -1) { + while (inputStream.read(buf).also { len = it } != -1) { if (isCancelled()) throw CancellationException() raf.write(buf, 0, len) - sum += len.toLong() + sum += len downloadSizeInfo[fileName] = sum - doProgress(sum) - } - Log.e(TAG, "download success") - if (!file.exists()) { - throw FileNotFoundException("file create err,not exists") - } else { + // 5. 性能节流:限制 UI 刷新频率(每100ms一次) + val now = System.currentTimeMillis() + if (now - lastUpdateTime > 100L || sum == currentLen + total) { + doProgress(sum) + lastUpdateTime = now + } + } + + if (file.exists()) { doSuccess(file) + // 6. 状态清理:成功后移除进度,防止下次下载“续传”到已完成文件的末尾 + downloadSizeInfo.remove(fileName) } - 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()) + Log.e(TAG, "Download failed: ${e.message}") + if (e !is CancellationException) { + if (deleteWhenException) file?.delete() + doException(e.message ?: "Unknown error") + } } finally { - try { - `is`?.close() - } catch (e: IOException) { - e.printStackTrace() - } - try { - raf?.close() - } catch (e: IOException) { - e.printStackTrace() - } + inputStream?.close() + raf?.close() } } - 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() + private fun isCanContinueDownload(url: URL, start: Long): Boolean { + return try { + val request = Request.Builder() + .url(url) + .header("RANGE", "bytes=$start-") + .head() + .build() + okHttpClient.newCall(request).execute().use { it.code == 206 } + } catch (e: Exception) { false } } private fun isDowning(tag: String): Boolean { - for (call in okHttpClient.value.dispatcher.runningCalls()) { - if (call.request().tag() == tag) { - return true - } - } - return false + return okHttpClient.dispatcher.runningCalls().any { it.request().tag() == tag } } - private fun isCancelled(): Boolean { - return cancelledList.contains(fileName) - } + private fun isCancelled() = cancelledList.contains(fileName) fun cancel() { - cancel(filePath + fileName) - } - - fun cancel(tag: String) { + val tag = filePath + fileName cancelledList.add(fileName) - if (okHttpClient.value.dispatcher.runningCalls().isNotEmpty()) { - for (call in okHttpClient.value.dispatcher.runningCalls()) { - if (call.request().tag() == tag) { - call.cancel() - } - } - } + okHttpClient.dispatcher.runningCalls().forEach { if (it.request().tag() == tag) it.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() + if (Looper.myLooper() == Looper.getMainLooper()) action() else mainHandler.post(action) } + private fun doException(err: String) = runOnUiThread { + downCallBack?.fail(err) ?: actionFail.invoke(err) + } + + private fun doSuccess(file: File) = runOnUiThread { + downCallBack?.success(file) ?: actionSuccess.invoke(file) + } + + private fun doGetTotal(total: Long) = runOnUiThread { + downCallBack?.total(total) ?: actionGetTotal.invoke(total) + } + + private fun doProgress(progress: Long) = runOnUiThread { + downCallBack?.progress(progress) ?: actionProgress.invoke(progress) + } + + private fun doCancel() = runOnUiThread { + downCallBack?.cancel() ?: actionCancel.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 - ) - ) + val request = chain.request() + val t1 = System.nanoTime() + Log.d(TAG, "Sending request ${request.url}") + val response = chain.proceed(request) + val t2 = System.nanoTime() + Log.d(TAG, "Received response for ${response.request.url} in ${(t2 - t1) / 1e6}ms") return response } - - companion object { - private const val TAG = "LoggingInterceptor" - } } interface DownCallBack { diff --git a/app/src/main/java/com/img/rabbit/utils/UpdateUtils.kt b/app/src/main/java/com/img/rabbit/utils/UpdateUtils.kt index e04aba7..26074b8 100644 --- a/app/src/main/java/com/img/rabbit/utils/UpdateUtils.kt +++ b/app/src/main/java/com/img/rabbit/utils/UpdateUtils.kt @@ -25,23 +25,27 @@ object UpdateUtils { } var totalProgress = 0L DownUtils.getInstance() - .setReadTImeOut(10L) - .setDeleteWhenException(false) .initUrl(url, null) .setFilePath(filePath) .setFileName(fileName) .setActionCallBack( - { totalProgress = it }, - { + getTotal = { totalProgress = it }, + progress = { val percent = it.toDouble() / totalProgress.toDouble() * 100 val curProgress = percent.toInt() onProgress(curProgress) }, - { + success = { onFinish(true, it.absolutePath) - }, { + }, + fail = { + if(destination.exists()){ + destination.delete() + } onFinish(false, null) - }).down() + } + ) + .down() /* applicationContext?.let {