Pause and resume downloads using Retrofit

后端 未结 2 1922
礼貌的吻别
礼貌的吻别 2020-12-30 15:49

I used this tutorial to implement downloading files in my app: https://www.learn2crack.com/2016/05/downloading-file-using-retrofit.html

The problem is that if intern

2条回答
  •  醉酒成梦
    2020-12-30 16:33

    This is my Kotlin implementation, inspired by Клаус Шварц:

    i used Coroutines because they make the code very easy to read and use; i also used ru.gildor.coroutines:kotlin-coroutines-retrofit to add coroutine support to retrofit.

    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::class.java) }
    
        val baseUrl = "http://www.your-website-base-url.com"
    
        private fun serviceBuilder(): Retrofit {
            //--- OkHttp client ---//
            val okHttpClient = OkHttpClient.Builder()
                    .readTimeout(60, TimeUnit.SECONDS)
                    .connectTimeout(60, TimeUnit.SECONDS)
    
            //--- Add authentication headers ---//
            okHttpClient.addInterceptor { chain ->
                val original = chain.request()
    
                // Just some example headers
                val requestBuilder = original.newBuilder()
                        .addHeader("Connection","keep-alive")
                        .header("User-Agent", "downloader")
    
                val request = requestBuilder.build()
                chain.proceed(request)
            }
    
            //--- Add logging ---//
    
            if (BuildConfig.DEBUG) {
                // development build
                val logging = HttpLoggingInterceptor()
                logging.setLevel(HttpLoggingInterceptor.Level.BASIC)
                // NOTE: do NOT use request BODY logging or it will not work!
    
                okHttpClient.addInterceptor(logging)
            }
    
    
            //--- Return Retrofit class ---//
            return Retrofit.Builder()
                    .client(okHttpClient.build())
                    .baseUrl(baseUrl)
                    .build()
        }
    
        suspend fun downloadOrResume(
                url:String, destination: File,
                headers:HashMap = HashMap(),
                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 = HashMap(),
                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,
                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)
                //java.lang.IllegalStateException: Error downloading file: 416, Requested Range Not Satisfiable; Response Response{protocol=http/1.1, code=416, message=Requested Range Not Satisfiable, u
            }
            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 {
                        //println("- Reading... $byteCount")
                        val bytesRead = super.read(sink, byteCount)
    
                        totalRead += bytesRead
    
                        val currentPercentage = (totalRead * 100 / totalBytes).toInt()
                        //println("Progress: $currentPercentage - $totalRead")
                        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
            ): Call
        }
    }
    

    Example usage:

        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")
    
        //Optional: you can also add custom headers
        val headers = HashMap()
    
        try {
            // Start or continue a download, catch download exceptions
            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

    dependencies {
        /** Retrofit 2 **/
        compile 'com.squareup.retrofit2:retrofit:2.4.0'
    
        // OkHttp for Retrofit request customization
        compile 'com.squareup.okhttp3:okhttp:3.10.0'
    
        // For http request logging
        compile 'com.squareup.okhttp3:logging-interceptor:3.10.0'
    
        // Retrofit Kotlin coroutines support
        compile 'ru.gildor.coroutines:kotlin-coroutines-retrofit:0.9.0'
    }
    

    Note: Kotlin coroutines must be enabled, at the moment they need to be enabled as experimental

提交回复
热议问题