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