diff --git a/app/src/main/java/com/img/rabbit/pages/dialog/UpdateDialog.kt b/app/src/main/java/com/img/rabbit/pages/dialog/UpdateDialog.kt index 8feacb6..dcee8d7 100644 --- a/app/src/main/java/com/img/rabbit/pages/dialog/UpdateDialog.kt +++ b/app/src/main/java/com/img/rabbit/pages/dialog/UpdateDialog.kt @@ -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) 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 0b2b95a..cf3fcb6 100644 --- a/app/src/main/java/com/img/rabbit/utils/DownUtils.kt +++ b/app/src/main/java/com/img/rabbit/utils/DownUtils.kt @@ -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() + private var cancelledList = mutableListOf() + + 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?): 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() + } + } + + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/img/rabbit/utils/UniAppUtils.kt b/app/src/main/java/com/img/rabbit/utils/UniAppUtils.kt index 44d4d19..a0f9008 100644 --- a/app/src/main/java/com/img/rabbit/utils/UniAppUtils.kt +++ b/app/src/main/java/com/img/rabbit/utils/UniAppUtils.kt @@ -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 } 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 b71b425..e04aba7 100644 --- a/app/src/main/java/com/img/rabbit/utils/UpdateUtils.kt +++ b/app/src/main/java/com/img/rabbit/utils/UpdateUtils.kt @@ -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) + } + }) + } + */ } } diff --git a/app/src/main/java/com/img/rabbit/viewmodel/CutoutViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/CutoutViewModel.kt index 1911d72..5534e66 100644 --- a/app/src/main/java/com/img/rabbit/viewmodel/CutoutViewModel.kt +++ b/app/src/main/java/com/img/rabbit/viewmodel/CutoutViewModel.kt @@ -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() diff --git a/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt index c1d0ec0..975dc08 100644 --- a/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt +++ b/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt @@ -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(){