使用 Retrofit 暂停和恢复下载

2023-11-23

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

问题是,如果网速很慢或者网络波动哪怕是一秒钟,下载就会永久停止。应用程序是否可以通过某种方式检测到互联网未处于活动状态(已连接但实际上网络无法正常工作),然后暂停下载并在互联网正常时恢复。

或者有一些替​​代方案,这样用户就不会感到沮丧?


我今天也遇到了这个问题,没有找到任何好的解决方案来实现立即下载恢复、进度通知和快速 nio 操作的 BufferedSink 使用。

这就是使用 Retrofit2 和 RxJava2 完成的方法。代码写在Kotlin适用于 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'
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 Retrofit 暂停和恢复下载 的相关文章

  • ScrollView 与relativelayout 不能很好地配合

    所以我有一个 ScrollView 作为布局中的最高级别 在我的 ScrollView 中我有一个包含其他视图的相对布局 问题在于 RelativeLayout 没有按应有的方式覆盖整个布局 它会在大约一半的屏幕处被截断 它的宽度是屏幕的整
  • 安卓。 onEditorAction 从未被调用

    我正在尝试捕捉从屏幕上移除键盘的事件 并且我正在使用OnEditorActionListener班级 然而 其onEditorAction方法永远不会被调用 这是我的 XML 格式的 EditText
  • 如何以编程方式检查应用程序是否在调试模式下运行?

    我必须在应用程序中的某个位置确定我的应用程序是在调试模式还是实时模式下运行 是否有任何函数或代码可用于检查 在开 关两种情况下都会返回 true false 如果是这样 请帮助我 提前致谢 从问题中尚不清楚调试模式是否指的是 应用程序是否可
  • Android ViewPager手动调用PageTransformer TransformPage

    我有一个带有 PageTransformer 实现的 ViewPager 它可以对 ViewPager 的下一页执行一些有趣的操作 我的实现如下所示 class ZoomOutPageTransformer implements ViewP
  • 从android中的另一个广播接收器注册广播接收器

    目前我有广播接收器用于监听呼叫状态事件 我已经注册了广播接收器AndroidManifest xml如下所示
  • Android 上的 setTimeOut() 相当于什么?

    我需要等效的代码setTimeOut call function milliseconds 对于安卓 setTimeOut call function milliseconds 您可能想查看定时任务 http developer andro
  • 如何从 Android 中的 DatePicker 小部件获取日期?

    I use a DatePickerAndroid中的小部件供用户设置日期 并希望在单击确认按钮时获取日期值 我该怎么做 尝试这个 DatePicker datePicker DatePicker findViewById R id dat
  • Android:使用 ActivityResultLauncher 时如何区分多个意图

    我正在创建一个意图选择器来在手机相机应用程序和图库 文件管理器之间进行选择 Intent chooserIntent Intent createChooser clickPhoto Set Image Using chooserIntent
  • 如何动态设置每个 Gridview 图像项的边框

    我想显示每个 gridview 项目的不同边框 我如何动态地为每个项目设置边框
  • BroadcastReceiver:以编程方式设置 android:process

    我希望我的应用程序能够检测外部存储的状态何时发生变化 首先在我的AndroidManifest xml中定义了一个BroadcastReceiver 这里我可以设置android process and android exported像这
  • “您的 APP_BUILD_SCRIPT 指向未知文件:./jni/Android.mk”

    我搜索了整个网络 我认为 不管它说了什么 我尝试过 但仍然没有运气 不工作 我在cygwin和windows下都测试了它这是我的项目文件 目录 D Java 2 workspace indigo JniTest classpath D Ja
  • 在 Android 中以高音量录制

    我正在使用 MediaRecorder 进行音频录制 但不幸的是 当我播放录制的音频时 我的媒体音量非常低 我什么也没听到 几乎什么也没听到 有没有办法提高录音音量 就像我们录音时的 setVolume 一样 我尝试在 MediaRecor
  • 将 Cordova console.log 写入文件

    有谁知道是否有可能console log写入文件或类似的东西 我已经记录了我的应用程序 但它仅写在控制台上 出于远程目的debugging我也需要将现有日志写入文件 我想创建一个文件并将日志写入该文件中 但这将使我复制现有的日志代码 因此
  • Android中如何使用ScrollView?

    我有一个 XML 布局文件 但文本超出了屏幕尺寸 我需要做什么才能制作ScrollView
  • 如何访问SD卡并返回特定格式的文件并进行数组?

    我需要访问SD卡并返回一些不同格式的文件 该位置将由用户输入 我如何以编程方式执行此操作 西蒙迪德 我相信这就是您正在寻找的 访问SD卡 在android中从sdcard读取特定文件 https stackoverflow com ques
  • Android studio SDK 管理器丢失

    我正在关注这个离子教程 https ionicacademy com get started with ionic 在 Android Studio 中设置 SDK 来测试我的 ionic 项目时遇到了一些问题 我正在寻找 SDK 管理器
  • 手势检测器不工作

    我有以下手势监听器 public class BookListener extends SimpleOnGestureListener implements OnTouchListener private LibraryActivity m
  • 如何在运行时将元数据信息写入Android Manifest

    我知道可以编辑 Android 清单组件 例如 将其设置为启用 禁用等 我想在运行时将元值标签插入到 Android 清单的应用程序标签中 我怎样才能直接写入android清单 这是我想直接写入我的应用程序的 Android 清单中的字符串
  • Spotify API:INVALID_APP_ID

    我目前正在开发一个实现 Spotify API 的 Android 应用程序 我已经使用教程将我的应用程序连接到 Spotify 的所有代码 并且已经在我的应用程序上工作了一段时间了 当我在验证用户身份后通过我的应用程序播放歌曲时 它在我的
  • androidx Recycler View 匹配约束 (0dp) 与换行内容行为

    我这里有简单的回收器视图 我想要的是 当列表很短时 将按钮粘贴到回收器视图下方 当列表很长时 将按钮粘在屏幕底部 但回收器视图正确换行并且能够滚动到底部

随机推荐

  • Python 中基于 Web 的聊天服务器的教程 [已关闭]

    Closed 此问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 目前不接受答案 我正在做一个网络课程的家庭作业项目 我们必须用 C C 或 Python 构建一个简单的基于 Web 的聊天服务器 我选择 Python 是因为我认
  • AVCaptureSession stopRunning 方法会造成严重的挂起

    Using iOS 7 教程第 22 章中的 Raywenderlich 二维码阅读器 我成功读取当前应用程序的二维码 我现在扩展它 在成功读取二维码后 我想存储stringValue of the AVMetadataMachineRea
  • 间隔顺序统计

    给定一个数字数组a 0 a 1 a n 1 我们得到这样的查询 output k 范围内的最大数字a i a i 1 a j 这些问题能否在多对数时间内得到回答 在n 每个查询 如果不是 是否有可能对结果进行平均并仍然获得良好的摊余复杂度
  • 具有直接像素访问的 Opencv 颜色映射

    我有一个灰度图像 我想通过将灰度值映射到调色板 如 Matlab 中的颜色图 来以彩色显示 我设法使用 OpenCV 做到了cvSet2D函数 但出于性能原因我想直接访问像素 但当我这样做时 图像有奇怪的颜色 我尝试以不同的顺序设置颜色 R
  • WPF 中文本框中的 ScrollToCaret 位于何处?

    我无法找到该功能 基本上我有一个多行文本框 当我执行搜索时 我会突出显示结果 但是 如果结果不在视图中 我将不得不手动向下滚动 直到找到突出显示的结果 这超出了 查找 功能的目的 我不想使用 RichTextBox 因为我遇到了一些性能问题
  • PHP PDO 获取 null

    如何检查列值是否为空 示例代码 db DBCxn getCxn sql SELECT exercise id author id submission result submission time total rating votes to
  • 单击按钮时呈现部分视图

    我有索引视图 using System Web Mvc Html model MsmqTestApp Models MsmqData Scripts jquery unobtrusive ajax min js type text java
  • python子进程Popen环境路径?

    假设有一个可执行文件和一个用于启动它的 Python 脚本 并且它们位于 兄弟 子目录中 例如 tmp subdir1 myexecutable tmp subdir2 myscript py If in tmp和跑步python subd
  • 如何强制 std::stringstream 运算符 >> 读取整个字符串?

    如何强制 std stringstream 运算符 gt gt 读取整个字符串而不是停在第一个空格处 我有一个模板类 用于存储从文本文件读取的值 template
  • VS2019中的C++20 chrono解析问题(最新)

    我有一个使用 date h 库在 C 14 下工作的函数 但我正在将程序转换为使用 C 20 但它不再工作 请问我做错了什么 我的C 14 date h代码如下 include
  • 以Python方式打印字典中最大值的键[重复]

    这个问题在这里已经有答案了 给定一本字典d其中键值对由字符串作为键和整数作为值组成 我想打印值为最大值的键字符串 当然我可以循环d items 存储最大值及其键 并在后面输出后者for环形 但是有没有一种更 Pythonic 的方式只使用m
  • 无法使用 SelectedItem = null 清除列表框选择 - MVVM

    我有以下数据模板 以及相应的视图模型 未显示
  • 使用 LINQ 获取具有自定义对象的列表的总和/平均值

    我有一堂课 public class PointD public double X get set public double Y get set public PointD double x double y X x Y y operat
  • 从 Python 打开 Excel 应用程序

    我正在使用 xlwt 写入 Excel 文件 作为我的 Python 项目的一部分 我还需要实际打开 Excel 电子表格进行显示并关闭它 我发现了一个函数 import webbrowser webbrowser open C Users
  • Delphi 中的命名空间

    使用长单位文件名是否有任何实际好处 例如MyLib MyUtils pas或者它只是一种单位名称前缀 与所有标识符一样 命名空间旨在进行组织 因此 只有当您的项目以更好的方式组织时 使用它们才会受益 这个高度主观的问题 即使是最简单的命名约
  • JQuery UI 选项卡:如何从另一个页面直接导航到选项卡?

    JQuery UI 选项卡是通过无序列表中的命名锚点实现的 当您将鼠标悬停在其中一个选项卡上时 您可以在浏览器底部显示的链接中看到这一点 http mysite product 3 orders 例如 上面是 订单 选项卡 JQuery 显
  • getAdapterPosition() 不返回 RecyclerView 中项目的位置

    这是一种后续或补充这个帖子 我试图获取 RecyclerView 中某个项目的位置 但我尝试过的方法都不起作用 我打了电话getAdapterPosition in my PersonViewHolder构造函数并将其值赋给整数位置 该位置
  • 是否有 Julia 相当于 NumPy 的省略号切片语法 (...)?

    在 NumPy 中 省略号语法 is for 填写若干 直到切片说明符的数量与数组的维度匹配 释义这个答案 我怎样才能在朱莉娅中做到这一点 还没有 但如果你愿意的话你可以帮助自己 import Base getindex Base seti
  • 如何重写 JavaScript 函数

    我正在尝试覆盖内置的parseFloat函数于JavaScript 我该怎么做呢 var origParseFloat parseFloat parseFloat function str alert And I m in your flo
  • 使用 Retrofit 暂停和恢复下载

    我使用本教程在我的应用程序中实现下载文件 https www learn2crack com 2016 05 downloading file using retrofit html 问题是 如果网速很慢或者网络波动哪怕是一秒钟 下载就会永