Kotlin, :
Coroutines, ;
ru.gildor.coroutines: kotlin-coroutines-retrofit, coroutine .
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import okhttp3.logging.HttpLoggingInterceptor
import okio.Buffer
import okio.BufferedSink
import okio.ForwardingSource
import okio.Okio
import retrofit2.Call
import retrofit2.HttpException
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.http.HeaderMap
import retrofit2.http.Streaming
import retrofit2.http.Url
import ru.gildor.coroutines.retrofit.awaitResponse
import java.io.File
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
object FileDownloader{
private val Service by lazy { serviceBuilder().create<FileDownloaderInterface>(FileDownloaderInterface::class.java) }
val baseUrl = "http://www.your-website-base-url.com"
private fun serviceBuilder(): Retrofit {
val okHttpClient = OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
okHttpClient.addInterceptor { chain ->
val original = chain.request()
val requestBuilder = original.newBuilder()
.addHeader("Connection","keep-alive")
.header("User-Agent", "downloader")
val request = requestBuilder.build()
chain.proceed(request)
}
if (BuildConfig.DEBUG) {
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BASIC)
okHttpClient.addInterceptor(logging)
}
return Retrofit.Builder()
.client(okHttpClient.build())
.baseUrl(baseUrl)
.build()
}
suspend fun downloadOrResume(
url:String, destination: File,
headers:HashMap<String,String> = HashMap<String,String>(),
onProgress: ((percent: Int, downloaded: Long, total: Long) -> Unit)? = null
){
var startingFrom = 0L
if(destination.exists() && destination.length()>0L){
startingFrom = destination.length()
headers.put("Range","bytes=${startingFrom}-")
}
println("Download starting from $startingFrom - headers: $headers")
download(url,destination,headers,onProgress)
}
suspend fun download(
url:String,
destination: File,
headers:HashMap<String,String> = HashMap<String,String>(),
onProgress: ((percent: Int, downloaded: Long, total: Long) -> Unit)? = null
) {
println("---------- downloadFileByUrl: getting response -------------")
val response = Service.downloadFile(url,headers).awaitResponse()
handleDownloadResponse(response,destination,onProgress)
}
fun handleDownloadResponse(
response:Response<ResponseBody>,
destination:File,
onProgress: ((percent: Int, downloaded: Long, total: Long) -> Unit)?
) {
println("-- downloadFileByUrl: parsing response! $response")
var startingByte = 0L
var endingByte = 0L
var totalBytes = 0L
if(!response.isSuccessful) {
throw HttpException(response)
}
val contentLength = response.body()!!.contentLength()
if (response.code() == 206) {
println("- http 206: Continue download")
val matcher = Pattern.compile("bytes ([0-9]*)-([0-9]*)/([0-9]*)").matcher(response.headers().get("Content-Range"))
if (matcher.find()) {
startingByte = matcher.group(1).toLong()
endingByte = matcher.group(2).toLong()
totalBytes = matcher.group(3).toLong()
}
println("Getting range from $startingByte to ${endingByte} of ${totalBytes} bytes" )
} else {
println("- new download")
endingByte = contentLength
totalBytes = contentLength
if (destination.exists()) {
println("Delete previous download!")
destination.delete()
}
}
println("Getting range from $startingByte to ${endingByte} of ${totalBytes} bytes" )
val sink: BufferedSink
if (startingByte > 0) {
sink = Okio.buffer(Okio.appendingSink(destination))
} else {
sink = Okio.buffer(Okio.sink(destination))
}
var lastPercentage=-1
var totalRead=startingByte
sink.use {
it.writeAll(object : ForwardingSource(response.body()!!.source()) {
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
totalRead += bytesRead
val currentPercentage = (totalRead * 100 / totalBytes).toInt()
if (currentPercentage > lastPercentage) {
lastPercentage = currentPercentage
if(onProgress!=null){
onProgress(currentPercentage,totalRead,totalBytes)
}
}
return bytesRead
}
})
}
println("--- Download complete!")
}
internal interface FileDownloaderInterface{
@Streaming
@GET
fun downloadFile(
@Url fileUrl: String,
@HeaderMap headers:Map<String,String>
): Call<ResponseBody>
}
}
:
val url = "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-9.4.0-amd64-xfce-CD-1.iso"
val destination = File(context.filesDir, "debian-9.4.0-amd64-xfce-CD-1.iso")
val headers = HashMap<String,String>()
try {
FileDownloader.downloadOrResume(
url,
destination,
headers,
onProgress = { progress, read, total ->
println(">>> Download $progress% ($read/$total b)")
});
}catch(e: SocketTimeoutException){
println("Download socket TIMEOUT exception: $e")
}catch(e: SocketException){
println("Download socket exception: $e")
}catch(e: HttpException){
println("Download HTTP exception: $e")
}
Gradle
dependencies {
compile 'com.squareup.retrofit2:retrofit:2.4.0'
compile 'com.squareup.okhttp3:okhttp:3.10.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.10.0'
compile 'ru.gildor.coroutines:kotlin-coroutines-retrofit:0.9.0'
}
: Kotlin coroutines ,