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

发布于:2025-03-18 ⋅ 阅读:(13) ⋅ 点赞:(0)

下载文件

/**
 * 文件下载工具
 */
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<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 = 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)
        }
    }