下载文件
/*** 文件下载工具*/
object DownloadUtil {private fun getOkHttpClient(): OkHttpClient {return OkHttpClient.Builder().addInterceptor(TokenIntercepter()).connectTimeout(20L, TimeUnit.SECONDS) // 连接超时.writeTimeout(20L, TimeUnit.SECONDS) // 写超时.readTimeout(60L, TimeUnit.SECONDS) // 读取超时.addNetworkInterceptor(LoggingInterceptor()) // 添加网络拦截器.build()}fun downloadFile(url: String,outputFile: File,onError: (String?) -> Unit,progressCallback: (bytesRead: Long, contentLength: Long, done: Boolean) -> Unit,): Call {val client = getOkHttpClient()// 如果文件已存在,则获取已下载的字节数val downloadedLength = if (outputFile.exists()) outputFile.length() else 0Lval requestBuilder = Request.Builder().url(url)if (downloadedLength > 0) {requestBuilder.header("Range", "bytes=$downloadedLength-")}val request = requestBuilder.build()val newCall = client.newCall(request)newCall.enqueue(object : Callback {override fun onFailure(call: Call, e: IOException) {e.printStackTrace()onError.invoke(e.message)}override fun onResponse(call: Call, response: Response) {response.body?.let { body ->// 判断是否为断点续传(206 状态码表示部分内容)val isResuming = response.code == 206LogUtils.d("DownloadUtil downloadFile $isResuming")val source = body.source()// 如果服务器不支持断点续传(返回 200),则重头开始下载val sink = if (isResuming) {// 采用追加模式写入文件outputFile.appendingSink().buffer()} else {// 如果文件已存在且不支持续传,则删除旧文件if (outputFile.exists()) {outputFile.delete()}outputFile.sink().buffer()}// 计算整个文件的总大小,如果是续传则为已下载字节数加上此次响应返回的数据长度val totalLength = if (isResuming) {downloadedLength + body.contentLength()} else {body.contentLength()}var totalBytesRead = 0Lval bufferSize = 32 * 1024Lvar bytesRead: Inttry {val buffer = ByteArray(bufferSize.toInt())while (source.read(buffer).also { bytesRead = it } != -1) {sink.write(buffer, 0, bytesRead) // 逐块写入文件totalBytesRead += bytesRead// 当前进度为:已下载字节 + 本次读取的总字节数(续传时)val currentProgress = if (isResuming) downloadedLength + totalBytesRead else totalBytesReadprogressCallback(currentProgress, totalLength, currentProgress == totalLength)}sink.flush()} catch (e: IOException) {e.printStackTrace()onError.invoke(e.message)} finally {sink.close()source.close()}}}})return newCall}
}
后台任务
class DownloadWorker(context: Context, params: WorkerParameters) :CoroutineWorker(context, params) {companion object {const val INPUT_DATA_URL = "url"const val INPUT_DATA_TARGET_FILE_PATH = "targetFile"const val OUTPUT_DATA_PROGRESS = "progress"const val OUTPUT_DATA_FILE_PATH = "filePath"const val UNIQUE_WORK_NAME_PRE = "download_task"/*** 启动下载任务* @param url 下载地址* @param fileName 目标文件名,下载目录存在 applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)* @param uniqueName UNIQUE_WORK_NAME_PRE+版本号名称 作为任务的唯一标识*/fun enqueueDownloadTask(context: Context,url: String,fileName: String,uniqueName: String): String {// 构造输入数据val inputData = workDataOf(INPUT_DATA_URL to url,INPUT_DATA_TARGET_FILE_PATH to fileName)val downloadRequest = OneTimeWorkRequestBuilder<DownloadWorker>().setInputData(inputData).setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()).build()// 使用唯一任务名称 "download_task",策略为 KEEP(已有则保持,不重新创建)WorkManager.getInstance(context).enqueueUniqueWork(uniqueName,ExistingWorkPolicy.REPLACE, //如果要重启的话 ExistingWorkPolicy.REPLACE,保持原来 ExistingWorkPolicy.KEEP,downloadRequest)// 返回任务的 UUID 字符串,便于后续观察进度return downloadRequest.id.toString()}fun cancelDownloadTask(context: Context, uniqueName: String) {WorkManager.getInstance(context).cancelUniqueWork(uniqueName)}}override suspend fun doWork(): Result {// 从输入数据获取下载地址和文件名val url = inputData.getString(INPUT_DATA_URL) ?: ""val fileName = inputData.getString(INPUT_DATA_TARGET_FILE_PATH) ?: ""LogUtils.v("DownloadWorker doWork fileName=$fileName,url=$url")if (url.isEmpty() || fileName.isEmpty()) return Result.failure()// 这里保存到 app 的 files 目录中,实际可根据需求修改val outputFile = File(fileName)return suspendCancellableCoroutine { continuation ->val call = DownloadUtil.downloadFile(url, outputFile,onError = { msg ->continuation.resume(Result.failure()) {LogUtils.e("DownloadWorker onError $msg")}}) { bytesRead, contentLength, done ->val progress = (bytesRead.toFloat() / contentLength * 100).toInt()setProgressAsync(workDataOf(OUTPUT_DATA_PROGRESS to progress))LogUtils.d("DownloadWorker DownloadProgress $progress,$bytesRead,$contentLength!")if (done) {val outputData = workDataOf(OUTPUT_DATA_FILE_PATH to outputFile.absolutePath)continuation.resume(Result.success(outputData)) {LogUtils.v("DownloadWorker success $fileName")}}}continuation.invokeOnCancellation {LogUtils.v("DownloadWorker invokeOnCancellation ")call.cancel()}}}}
初始化的时候,检查是否存在任务
/*** 检查下载任务uniqueWorkName 是否存在*/fun checkDownloadTask(context: Context,lifecycleOwner: LifecycleOwner,uniqueWorkName: String) {// 观察唯一任务的状态checkDownloadJob?.cancel()checkDownloadJob = viewModelScope.launch {// 此处使用 getWorkInfosForUniqueWork 监听所有同名任务(通常只有一个任务)WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData(uniqueWorkName).observe(lifecycleOwner) { workInfos ->LogUtils.d("InAppUpdateComposeViewModel checkDownloadTask size=${workInfos.size}")if (workInfos.isNullOrEmpty()) {//没有任务} else {// 这里取第一个任务val workInfo = workInfos.first()LogUtils.d("InAppUpdateComposeViewModel checkDownloadTask workInfo=${workInfo.state}")when (workInfo.state) {WorkInfo.State.ENQUEUED -> {//排队中_updateStatusFlow.value = UpdateStatus.DOWNLOADING}WorkInfo.State.RUNNING -> {//下载中_updateStatusFlow.value = UpdateStatus.DOWNLOADING// 更新进度val progress =workInfo.progress.getInt(DownloadWorker.OUTPUT_DATA_PROGRESS, 0)_progressFlow.value = progressLogUtils.d("InAppUpdateComposeViewModel checkDownloadTask progress=${progress}")}WorkInfo.State.SUCCEEDED -> {//下载成功val apkFilePath =workInfo.outputData.getString(DownloadWorker.OUTPUT_DATA_FILE_PATH)val apkFile = File(apkFilePath)if (apkFile.exists()) {_updateStatusFlow.value = UpdateStatus.DOWNLOAD_SUCCESS_downloadResult.value =workInfo.outputData.getString(DownloadWorker.OUTPUT_DATA_FILE_PATH)} else {//任务成功但是文件不见了_updateStatusFlow.value = UpdateStatus.FILE_MISSING}}WorkInfo.State.FAILED -> {_updateStatusFlow.value = UpdateStatus.DOWNLOAD_FAILED}WorkInfo.State.CANCELLED -> {_updateStatusFlow.value = UpdateStatus.DOWNLOAD_CANCEL}else -> {_updateStatusFlow.value = UpdateStatus.DOWNLOADING}}}}}}
启动下载任务
fun startDownload(context: Context) {versionState.value?.apply {val uniqueWorkName = DownloadWorker.UNIQUE_WORK_NAME_PRE + newVersionval apkVersionName = "update_$newVersion.apk"val downloadDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)deleteOtherVersionApk(downloadDir, apkVersionName, "update_.*\\.apk")DownloadWorker.enqueueDownloadTask(context = context,url = url,fileName = "${context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)}/$apkVersionName",uniqueName = uniqueWorkName)}}
取消任务
fun cancelDownload(context: Context) {versionState.value?.apply {val uniqueWorkName = DownloadWorker.UNIQUE_WORK_NAME_PRE + newVersionDownloadWorker.cancelDownloadTask(context, uniqueWorkName)}}