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

下载文件

Kotlin 复制代码
/**
 * 文件下载工具
 */
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
    }
}

后台任务

Kotlin 复制代码
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()
            }
        }
    }

}

初始化的时候,检查是否存在任务

Kotlin 复制代码
 /**
     * 检查下载任务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
                            }
                        }
                    }
                }
        }
    }

   

启动下载任务

Kotlin 复制代码
 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
            )
        }
    }

取消任务

Kotlin 复制代码
    fun cancelDownload(context: Context) {
        versionState.value?.apply {
            val uniqueWorkName = DownloadWorker.UNIQUE_WORK_NAME_PRE + newVersion
            DownloadWorker.cancelDownloadTask(context, uniqueWorkName)
        }
    }

相关推荐
Kapaseker12 分钟前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴16 分钟前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭10 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab11 小时前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe16 小时前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
鹏程十八少1 天前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试
Kapaseker1 天前
一杯美式搞定 Kotlin 空安全
android·kotlin
三少爷的鞋1 天前
Android 协程时代,Handler 应该退休了吗?
android
火柴就是我2 天前
让我们实现一个更好看的内部阴影按钮
android·flutter