package com.img.rabbit.utils import android.os.Handler import android.os.Looper import android.util.Log import okhttp3.* import okhttp3.Headers.Companion.toHeaders import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import java.io.* import java.net.URL import java.util.concurrent.CancellationException import java.util.concurrent.TimeUnit class DownLoadUtils private constructor() { companion object { private const val TAG = "DownLoadUtils" private val downLoadHttpUtils: DownLoadUtils by lazy { DownLoadUtils() } @JvmStatic @Synchronized fun getInstance(): DownLoadUtils { return downLoadHttpUtils } } private var downloadSizeInfo = mutableMapOf() private var cancelledList = mutableListOf() private var buffSize = 4096//建议设置为2048 fun setBuffSize(size: Int): DownLoadUtils { this.buffSize = size return this } private var interceptor: Interceptor? = null fun setInterceptor(interceptor: Interceptor?): DownLoadUtils { this.interceptor = interceptor return this } private var readTimeOut = 30L fun setReadTImeOut(read: Long): DownLoadUtils { this.readTimeOut = read return this } private var writeTimeout = 30L fun setWriteTimeOut(write: Long): DownLoadUtils { this.writeTimeout = write return this } private var connectTimeout = 30L fun setConnectTimeOut(connect: Long): DownLoadUtils { this.connectTimeout = connect return this } var filePath = "" fun setFilePath(path: String): DownLoadUtils { this.filePath = path return this } private var fileName = "" fun setFileName(name: String): DownLoadUtils { this.fileName = name return this } private var deleteWhenException = true fun setDeleteWhenException(dele: Boolean): DownLoadUtils { this.deleteWhenException = dele return this } private val requestBuilder: Request.Builder = Request.Builder() private var urlBuilder: HttpUrl.Builder? = null private val okHttpClient = lazy { OkHttpClient.Builder() .readTimeout(readTimeOut, TimeUnit.SECONDS) .writeTimeout(writeTimeout, TimeUnit.SECONDS) .connectTimeout(connectTimeout, TimeUnit.SECONDS) .addInterceptor(interceptor ?: LoggingInterceptor()) .build() } private var actionGetTotal: (total: Long) -> Unit? = { _ -> } private var actionProgress: (position: Long) -> Unit? = { _ -> } private var actionSuccess: (file: File) -> Unit? = { _ -> } private var actionCancel: () -> Unit? = { } private var actionFail: (msg: String) -> Unit? = { _ -> } fun setActionCallBack( actionGetTotal: (total: Long) -> Unit, actionProgress: (position: Long) -> Unit, actionSuccess: (file: File) -> Unit, actionFail: (msg: String) -> Unit, ): DownLoadUtils { this.actionGetTotal = actionGetTotal this.actionProgress = actionProgress this.actionSuccess = actionSuccess this.actionFail = actionFail return this } private var downCallBack: DownCallBack? = null fun setDownCallBack(callBack: DownCallBack): DownLoadUtils { this.downCallBack = callBack return this } fun initUrl(url: String, params: Map?): DownLoadUtils { urlBuilder = url.toHttpUrlOrNull()?.newBuilder() if (params.isNullOrEmpty()) { return this } else { for ((k, v) in params) { checkName(k) urlBuilder?.setQueryParameter(k, v) } } return this } fun addHeader(map: Map): DownLoadUtils { requestBuilder.headers(map.toHeaders()) return this } private fun checkName(name: String) { require(name.isNotEmpty()) { "name is empty" } } fun down() { if (urlBuilder == null) { throw IllegalStateException("url not init") } else { doDown() } } private fun doDown() { val startTime = System.currentTimeMillis() Log.i(TAG, "startTime=$startTime") val url = urlBuilder?.build() if (url == null) { doException("url is null") return } if (isDowning(filePath + fileName)) { return } cancelledList.remove(fileName) val currentLen = downloadSizeInfo[fileName] ?: 0L if (isCanContinueDownload(url.toUrl(), currentLen)) { requestBuilder.removeHeader("RANGE") requestBuilder.addHeader("RANGE", "bytes=${currentLen}-") } val request = requestBuilder.url(url).tag(filePath + fileName).build() var `is`: InputStream? = null var raf: RandomAccessFile? = null var file: File? = null try { val response = okHttpClient.value.newCall(request).execute() val total = response.body?.contentLength() ?: 0 doGetTotal(currentLen + total) val buf = ByteArray(buffSize) var len: Int file = if (fileName.isEmpty()) { File(filePath) } else { val fileDir = File(filePath) if (!fileDir.exists() || !fileDir.isDirectory) { fileDir.mkdirs() } File(filePath, fileName) } `is` = response.body?.byteStream() ?: FileInputStream("") raf = RandomAccessFile(file, "rw") raf.seek(currentLen) var sum: Long = currentLen while (`is`.read(buf).also { len = it } != -1) { if (isCancelled()) throw CancellationException() raf.write(buf, 0, len) sum += len.toLong() downloadSizeInfo[fileName] = sum Log.e(TAG, "download progress : $sum") doProgress(sum) } Log.e(TAG, "download success") if (!file.exists()) { throw FileNotFoundException("file create err,not exists") } else { doSuccess(file) } Log.e(TAG, "totalTime=" + (System.currentTimeMillis() - startTime)) } catch (e: Exception) { if (deleteWhenException && file?.exists() == true) file.delete() Log.e(TAG, "download failed : " + e.message) doException(e.message.toString()) } finally { try { `is`?.close() } catch (e: IOException) { e.printStackTrace() } try { raf?.close() } catch (e: IOException) { e.printStackTrace() } } } private fun isCanContinueDownload(url: URL, start: Long): Boolean { //是否支持断点下载 val requestBuilder = Request.Builder() requestBuilder.addHeader("RANGE", "bytes=$start-") requestBuilder.addHeader("Connection", "close") val request: Request = requestBuilder.url(url).head().build() val response: Response = okHttpClient.value.newCall(request).execute() return if (response.isSuccessful) { if (response.code == 206) { //支持 response.close() true } else { //不支持 response.close() false } } else { response.close() false } } private fun isDowning(tag: String): Boolean { for (call in okHttpClient.value.dispatcher.runningCalls()) { if (call.request().tag() == tag) { return true } } return false } private fun isCancelled(): Boolean { return cancelledList.contains(fileName) } fun cancel() { cancel(filePath + fileName) } fun cancel(tag: String) { cancelledList.add(fileName) if (okHttpClient.value.dispatcher.runningCalls().isNotEmpty()) { for (call in okHttpClient.value.dispatcher.runningCalls()) { if (call.request().tag() == tag) { call.cancel() } } } doCancel() } private fun doException(err: String) { runOnUiThread { if (downCallBack == null) { actionFail.invoke(err) } else { downCallBack?.fail(err) } mainThread = null } } private fun doSuccess(file: File?) { runOnUiThread { if (file == null) { doException("file not exit") } else { if (downCallBack == null) { actionSuccess.invoke(file) } else { downCallBack?.success(file) } } mainThread = null } } private fun doGetTotal(total: Long) { runOnUiThread { if (downCallBack == null) { actionGetTotal.invoke(total) } else { downCallBack?.total(total) } } mainThread = null } private fun doProgress(progress: Long) { runOnUiThread { if (downCallBack == null) { actionProgress.invoke(progress) } else { downCallBack?.progress(progress) } } } private fun doCancel() { runOnUiThread { if (downCallBack == null) { actionCancel.invoke() } else { downCallBack?.cancel() } } mainThread = null } private var mainThread: Handler? = null private fun runOnUiThread(action: () -> Unit) { if (Looper.myLooper() != Looper.getMainLooper()) { // If we finish marking off of the main thread, we need to // actually do it on the main thread to ensure correct ordering. if (mainThread == null) { mainThread = Handler(Looper.getMainLooper()) } mainThread?.post { action.invoke() } return } action.invoke() } class LoggingInterceptor : Interceptor { @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val request: Request = chain.request() val startTime = System.nanoTime() Log.d( TAG, String.format( "Sending request %s on %s%n%s", request.url, chain.connection(), request.headers ) ) val response: Response = chain.proceed(request) val endTime = System.nanoTime() Log.d( TAG, String.format( "Received response for %s in %.1fms%n%s", response.request.url, (endTime - startTime) / 1e6, response.headers ) ) return response } companion object { private const val TAG = "LoggingInterceptor" } } interface DownCallBack { fun success(file: File) fun fail(str: String) fun progress(position: Long) fun total(total: Long) fun cancel() } }