使用Retrofit暂停和恢复下载

时间:2016-10-07 11:13:12

标签: android retrofit

我使用本教程在我的应用中实现下载文件:https://www.learn2crack.com/2016/05/downloading-file-using-retrofit.html

问题是,如果互联网速度很慢或网络即使一秒钟波动,下载也会永久停止。是否有某种方式应用程序可以检测到互联网不活动(连接但实际上网络不工作),然后暂停下载并恢复互联网正常。

或某些替代方案,以免用户感到沮丧?

2 个答案:

答案 0 :(得分:2)

我今天也遇到了这个问题,并没有找到任何好的解决方案,可以立即实现下载简历,进度通知和BufferedSink用于快速nio操作。

这是如何使用Retrofit2和RxJava2完成的。代码在Kotlin for Android中有效,但可以轻松移植到纯JVM:只需摆脱AndroidSchedulers

代码可能包含错误,因为它是在短时间内从头开始编写的,几乎没有经过测试。

import com.google.gson.GsonBuilder
import io.reactivex.Observable
import io.reactivex.ObservableEmitter
import io.reactivex.ObservableOnSubscribe
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.Consumer
import io.reactivex.functions.Function
import io.reactivex.schedulers.Schedulers
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import okio.Buffer
import okio.BufferedSink
import okio.ForwardingSource
import okio.Okio
import org.slf4j.LoggerFactory
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Streaming
import retrofit2.http.Url
import java.io.File
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern

class FileDownloader(val baseUrl: String) {

    private val log = LoggerFactory.getLogger(FileDownloader::class.java)

    private val expectedFileLength = ConcurrentHashMap<String, Long>()
    private val eTag = ConcurrentHashMap<String, String>()

    private val apiChecker: FileDownloaderAPI

    init {
        apiChecker = Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(OkHttpClient())
                .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
                .build()
                .create(FileDownloaderAPI::class.java)

    }


    /**
     *
     * @return File Observable
     */
    fun download(
            urlPath: String,
            file: File,
            dlProgressConsumer: Consumer<Int>): Observable<File> {
        return Observable.create(ObservableOnSubscribe<File> {
            val downloadObservable: Observable<Int>

            if (file.exists() &&
                    file.length() > 0L &&
                    file.length() != expectedFileLength[file.name]
                    ) {
                /**
                 * Try to get rest of the file according to:
                 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
                 */
                downloadObservable = apiChecker.downloadFile(
                        urlPath,
                        "bytes=${file.length()}-",
                        eTag[file.name] ?: "0"
                ).flatMap(
                        DownloadFunction(file, it)
                )
            } else {
                /**
                 * Last time file was fully downloaded or not present at all
                 */
                if (!file.exists())
                    eTag[file.name] = ""

                downloadObservable = apiChecker.downloadFile(
                        urlPath,
                        eTag[file.name] ?: "0"
                ).flatMap(
                        DownloadFunction(file, it)
                )

            }

            downloadObservable
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(dlProgressConsumer)

        }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
    }

    private inner class DownloadFunction(
            val file: File,
            val fileEmitter: ObservableEmitter<File>
    ) : Function<Response<ResponseBody>, Observable<Int>> {

        var contentLength = 0L

        var startingByte = 0L
        var endingByte = 0L
        var totalBytes = 0L


        var contentRangePattern = "bytes ([0-9]*)-([0-9]*)/([0-9]*)"
        fun parseContentRange(contentRange: String) {
            val matcher = Pattern.compile(contentRangePattern).matcher(contentRange)
            if (matcher.find()) {
                startingByte = matcher.group(1).toLong()
                endingByte = matcher.group(2).toLong()
                totalBytes = matcher.group(3).toLong()
            }
        }

        var totalRead = 0L

        var lastPercentage = 0

        override fun apply(response: Response<ResponseBody>): Observable<Int> {
            return Observable.create { subscriber ->
                try {
                    if (!response.isSuccessful) {
                        /**
                         * Including response 304 Not Modified
                         */
                        fileEmitter.onError(IllegalStateException("Code: ${response.code()}, ${response.message()}; Response $response"))
                        return@create
                    }

                    contentLength = response.body().contentLength()


                    log.info("{}", response)
                    /**
                     * Receiving partial content, which in general means that download is resumed
                     */
                    if (response.code() == 206) {
                        parseContentRange(response.headers().get("Content-Range"))
                        log.debug("Getting range from {} to {} of {} bytes", startingByte, endingByte, totalBytes)
                    } else {
                        endingByte = contentLength
                        totalBytes = contentLength
                        if (file.exists())
                            file.delete()
                    }

                    log.info("Starting byte: {}, ending byte {}", startingByte, endingByte)

                    totalRead = startingByte

                    eTag.put(file.name, response.headers().get("ETag"))
                    expectedFileLength.put(file.name, totalBytes)


                    val sink: BufferedSink
                    if (startingByte > 0) {
                        sink = Okio.buffer(Okio.appendingSink(file))
                    } else {
                        sink = Okio.buffer(Okio.sink(file))
                    }

                    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

                                /**
                                 * May not wok good if we get some shit from the middle of the file,
                                 * though that's not the case of this function, as we plan only to
                                 * resume downloads
                                 */
                                val currentPercentage = (totalRead * 100 / totalBytes).toInt()
                                if (currentPercentage > lastPercentage) {
                                    val progress = "$currentPercentage%"
                                    lastPercentage = currentPercentage
                                    subscriber.onNext(currentPercentage)
                                    log.debug("Downloading {} progress: {}", file.name, progress)
                                }
                                return bytesRead
                            }
                        })
                    }

                    subscriber.onComplete()
                    fileEmitter.onNext(file)
                    fileEmitter.onComplete()
                } catch (e: IOException) {
                    log.error("Last percentage: {}, Bytes read: {}", lastPercentage, totalRead)
                    fileEmitter.onError(e)
                }
            }
        }

    }

    interface FileDownloaderAPI {


        @Streaming @GET
        fun downloadFile(
                @Url fileUrl: String,
                @Header("If-None-Match") eTag: String
        ): Observable<Response<ResponseBody>>

        @Streaming @GET
        fun downloadFile(
                @Url fileUrl: String,

                // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
                @Header("Range") bytesRange: String,

                // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.27
                @Header("If-Range") eTag: String
        ): Observable<Response<ResponseBody>>
    }
}

然后在你想要的地方使用它

    val fileDownloader = FileDownloader("http://wwww.example.com")
    fileDownloader.download(
            "/huge-video.mkv",
            File("file-where-I-will-save-this-video.mkv"),
            Consumer { progress ->
                updateProgressNotificatuin()
            }
    ).subscribe({
        log.info("File saved at path {}", it.absolutePath)
    },{
        log.error("Download error {}", it.message, it)
    },{
        log.info("Download completed")
    })

此示例中使用的依赖项:

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:1.1.1"
    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'

    compile 'com.squareup.retrofit2:retrofit:2.2.0'
    compile 'com.squareup.retrofit2:converter-gson:2.2.0'
    compile 'com.squareup.retrofit2:adapter-rxjava2:2.2.0'
    compile 'com.google.code.gson:gson:2.7'


    compile 'org.slf4j:slf4j-api:1.7.25'
}

答案 1 :(得分:0)

这是我的Kotlin实施,受КлаусШварц的启发:

我使用了Coroutines,因为它们使代码非常易于阅读和使用; 我还使用了ru.gildor.coroutines:kotlin-coroutines-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>(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<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)
            //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<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")

    //Optional: you can also add custom headers
    val headers = HashMap<String,String>()

    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 {
    /** 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'
}

注意:必须启用Kotlin协同程序,目前需要将其作为实验启用