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)
        }
    }

相关推荐
Libraeking9 小时前
破壁行动:在旧项目中丝滑嵌入 Compose(混合开发实战)
android·经验分享·android jetpack
市场部需要一个软件开发岗位10 小时前
JAVA开发常见安全问题:Cookie 中明文存储用户名、密码
android·java·安全
JMchen12311 小时前
Android后台服务与网络保活:WorkManager的实战应用
android·java·网络·kotlin·php·android-studio
crmscs12 小时前
剪映永久解锁版/电脑版永久会员VIP/安卓SVIP手机永久版下载
android·智能手机·电脑
localbob12 小时前
杀戮尖塔 v6 MOD整合版(Slay the Spire)安卓+PC端免安装中文版分享 卡牌肉鸽神作!杀戮尖塔中文版,电脑和手机都能玩!杀戮尖塔.exe 杀戮尖塔.apk
android·杀戮尖塔apk·杀戮尖塔exe·游戏分享
机建狂魔12 小时前
手机秒变电影机:Blackmagic Camera + LUT滤镜包的专业级视频解决方案
android·拍照·摄影·lut滤镜·拍摄·摄像·录像
hudawei99612 小时前
flutter和Android动画的对比
android·flutter·动画
lxysbly14 小时前
md模拟器安卓版带金手指2026
android
儿歌八万首15 小时前
硬核春节:用 Compose 打造“赛博鞭炮”
android·kotlin·compose·春节
消失的旧时光-194317 小时前
从 Kotlin 到 Dart:为什么 sealed 是处理「多种返回结果」的最佳方式?
android·开发语言·flutter·架构·kotlin·sealed