目录

android-后台下载任务,断点续传

android 后台下载任务,断点续传

下载文件

/**

  • 文件下载工具 */ 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 0L val 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 == 206 LogUtils.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 = 0L val bufferSize = 32 * 1024L var bytesRead: Int try { 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 totalBytesRead progressCallback( 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() .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 = progress LogUtils.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 + newVersion val 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 + newVersion DownloadWorker.cancelDownloadTask(context, uniqueWorkName) } }