Newer
Older
CasicSmartTube / app / src / main / java / com / casic / smarttube / utils / FileDownloadManager.kt
package com.casic.smarttube.utils

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.File
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean

class FileDownloadManager(builder: Builder) {

    private val httpClient by lazy { OkHttpClient() }

    class Builder {
        lateinit var url: String
        lateinit var suffix: String
        lateinit var directory: File
        lateinit var downloadListener: OnFileDownloadListener

        /**
         * 文件下载地址
         * */
        fun setDownloadFileSource(url: String): Builder {
            this.url = url
            return this
        }

        /**
         * 文件后缀
         *  如:apk等
         * */
        fun setFileSuffix(suffix: String): Builder {
            this.suffix = if (suffix.contains(".")) {
                //去掉前缀的点
                suffix.drop(1)
            } else {
                suffix
            }
            return this
        }

        /**
         * 文件保存的地址
         * */
        fun setFileSaveDirectory(directory: File): Builder {
            this.directory = directory
            return this
        }

        /**
         * 设置文件下载回调监听
         * */
        fun setOnFileDownloadListener(downloadListener: OnFileDownloadListener): Builder {
            this.downloadListener = downloadListener
            return this
        }

        fun build(): FileDownloadManager {
            if (!::url.isInitialized || !::suffix.isInitialized || !::directory.isInitialized || !::downloadListener.isInitialized) {
                throw IllegalStateException("All properties must be initialized before building.")
            }
            return FileDownloadManager(this)
        }
    }

    private val url = builder.url
    private val suffix = builder.suffix
    private val directory = builder.directory
    private val listener = builder.downloadListener

    /**
     * 开始下载
     * */
    fun start() {
        val job = SupervisorJob()
        val scope = CoroutineScope(Dispatchers.Main + job)

        val request = Request.Builder().get().url(url).build()
        val newCall = httpClient.newCall(request)
        val isExecuting = AtomicBoolean(false)

        /**
         * 如果已被加入下载队列,则取消之前的,重新下载
         */
        if (isExecuting.getAndSet(true)) {
            newCall.cancel()
        }

        //异步下载文件
        newCall.enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                scope.launch(Dispatchers.Main) {
                    listener.onFailed(e)
                }
            }

            override fun onResponse(call: Call, response: Response) {
                scope.launch(Dispatchers.IO) {
                    val body = response.body
                    if (body == null) {
                        listener.onFailed(IOException("Response body is null"))
                        throw IOException("Response body is null")
                    } else {
                        val inputStream = body.byteStream()
                        val fileSize = body.contentLength()

                        if (fileSize <= 0) {
                            throw IllegalArgumentException("Invalid file size")
                        }
                        listener.onDownloadStart(fileSize)

                        val file = File(directory, "${System.currentTimeMillis()}.${suffix}")
                        file.outputStream().use { fos ->
                            val buffer = ByteArray(2048)
                            var sum = 0L
                            var read: Int
                            while (inputStream.read(buffer).also { read = it } != -1) {
                                fos.write(buffer, 0, read)
                                sum += read.toLong()
                                withContext(Dispatchers.Main) {
                                    listener.onProgressChanged(sum)
                                }
                            }
                        }

                        withContext(Dispatchers.Main) {
                            listener.onDownloadEnd(file)
                        }

                        job.cancel()
                    }
                }
            }
        })
    }

    interface OnFileDownloadListener {
        fun onDownloadStart(total: Long)
        fun onProgressChanged(progress: Long)
        fun onDownloadEnd(file: File)
        fun onFailed(t: Throwable)
    }
}