rabbit-android/app/src/main/java/com/img/rabbit/utils/DownLoadUtils.kt

385 lines
11 KiB
Kotlin

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<String, Long>()
private var cancelledList = mutableListOf<String>()
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<String, String>?): 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<String, String>): 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()
}
}