Android使用OkHttp3实现多文件下载、断点续传

最近在音乐app中遇到在线歌曲下载问题,于是有了这篇文章。这篇文章借鉴了https://blog.csdn.net/cfy137000/article/details/54838608,在此感谢,自己在此基础上修改了一点。

代码用Kotlin写的,这里只上核心代码。

class DownloadManager {

    //这里采用单例模式
    companion object {
        private val INSTANCE:AtomicReference<DownloadManager> = AtomicReference()
        fun getInstance(): DownloadManager{
            while(true) {
                var current: DownloadManager? = INSTANCE.get()//此处get方法可能返回null,current应为可空类型
                if (current != null) {
                    return current
                }
                current = DownloadManager()
                //compareAndSet:将INSTANCE中存放的值与第一个参数比较,如果相同则返回true并用第二个参数更新INSTANCE中的值,
                //如果不相同,返回false
                if (INSTANCE.compareAndSet(null, current)) {
                    return current
                }
            }
        }
    }
    //用哈希表存放下载时的网络请求
    private var downCalls: HashMap<String,Call> = HashMap<String,Call>()
    var client: OkHttpClient? = null     //全局使用同一个OkHttpClient
    //哈希表存放监听者,以URL为key,ArrayList允许存在想要监听同一个URL的Listener,相当于观察者模式
    var listeners: HashMap<String,ArrayList<DownloadListener>> = HashMap()

    constructor (){
        client = OkHttpClient.Builder().build()
    }
    //外界调用的下载函数,这里下载歌曲,传入的是歌曲的信息Song类,这里不展示Song类,可根据自己的情况更改
    fun downloadSong(song: Song){
        Log.e("DownloadManager","downloadSong")
        getSongNetInfo(song)
    }
    //根据歌曲信息,通过网络请求得到歌曲和歌词的下载链接
    fun getSongNetInfo(song: Song){
        Log.e("DownloadManager","getSongNetInfo")
        RetrofitFactory.provideBaiduApi()
                .querySong(song.getSong_id())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(object : Observer<SongPlayResp> {
                    override fun onSubscribe(d: Disposable) {}
                    override fun onNext(resp: SongPlayResp) {
                        if (resp != null && resp.isValid) {
                            song.bitrate = resp.bitrate
                            song.songInfo = resp.songinfo
                            var downloadInfo = createSongDownloadInfo(song)//构造downloadInfo
                            download(song,downloadInfo)  //得到下载链接后,开始下载
                        }
                    }
                    override fun onError(e: Throwable) {}
                    override fun onComplete() {}
                })
    }

    private fun download(song: Song, downloadInfo: DownloadInfo?) {
        if(downloadInfo == null || downloadInfo.url.equals(""))return
        Log.e("DownloadManager","download")
        Observable.just(downloadInfo)
                .filter { !downCalls.containsKey(it.url) }    //如果已经在下载,则过滤掉
                .flatMap { it -> Observable.create(DownloadSubscribe(it)) }//创建被观察者
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(object: DownloadObserver(downloadInfo){//构造函数传入downloadInfo
                    override fun onSubscribe(d: Disposable) {
                        super.onSubscribe(d)
                    }

                    override fun onNext(t: DownloadInfo) {
                        //根据downloadInfo的url,查找listener
                        //实时返回进度信息,信息存放在downloadInfo中
                        super.onNext(t)
                        Log.e("DownloadManager","正在下载..."+t.getDown())
                        var specListeners = listeners.get(t.url)
                        if(specListeners == null)return
                        for(l in specListeners){
                            l.onNext(t)
                        }
                    }

                    override fun onError(e: Throwable) {
                        super.onError(e)
                    }

                    override fun onComplete() {
                        super.onComplete()
                        //mInfo是构造函数传入的downloadInfo
                        if(mInfo == null)return
                        var specListeners = listeners.get(mInfo!!.url)
                        if(specListeners == null)return
                        for(l in specListeners){
                            l.onComplete(mInfo)
                        }
                    }
                })
    }

    //查找文件并获取文件已有长度
    private fun getDownloadInfo(downloadInfo: DownloadInfo?){
        var contentLength:Long = getContentLength(downloadInfo!!.url)
        downloadInfo!!.total = contentLength
        var file:File = File(downloadInfo.dir,downloadInfo.fileName)
        if(file.exists()){
            downloadInfo.progress = file.length()
        }
        if(downloadInfo.internal!=null)getDownloadInfo(downloadInfo.internal!!)
    }

    //获取网络资源的大小
    private fun getContentLength(url: String): Long {
        var request: Request = Request.Builder()
                .url(url)
                .build()
        try {
            Log.e("getContentLength","执行")
            if(client == null)Log.e("getContentLength","client is null!")

            var response: Response = client!!.newCall(request).execute()
            Log.e("getContentLength","执行完成")
            if (response != null && response.isSuccessful) {
                var contentLength: Long = response.body()!!.contentLength()
                response.close()
                if (contentLength == 0L) return DownloadInfo.TOTAL_ERROR
                return contentLength
            }
        }catch (e:IOException){
            e.printStackTrace()
        }
        return DownloadInfo.TOTAL_ERROR
    }

    //这里downloadInfo使用装饰者模式
    private fun createSongDownloadInfo(song: Song): DownloadInfo? {
        var downloadInfo: DownloadInfo? = null
        if(song.bitrate!=null && song.bitrate.file_link!=null && !song.bitrate.file_link.equals("")){
            downloadInfo = DownloadInfo(song.bitrate.file_link,null)//歌曲的downloadinfo
            downloadInfo.fileName = FileMusicUtils.getLocalMusicName(song.title,song.artist)
            downloadInfo.dir = FileMusicUtils.getLocalMusicDir()
            if(!TextUtils.isEmpty(song.lrclink)){
                var inter = DownloadInfo(song.lrclink,null)//歌词的downloadInfo
                inter.fileName = FileMusicUtils.getLrcFileName(song.title, song.artist)
                inter.dir = FileMusicUtils.getLrcDir()
                downloadInfo.internal = inter
            }
        }
        return downloadInfo

    }
    //添加观察者
    fun addListener(urlString: String,listener: DownloadListener?){
        if(listener==null)return
        if(listeners.containsKey(urlString)){
            listeners.get(urlString)?.add(listener)
        }else{
            var array = ArrayList<DownloadListener>()
            array.add(listener!!)
            listeners.put(urlString,array)
        }
    }
    //移除观察者
    fun removeListener(urlString: String,listener: DownloadListener?){
        if(listener==null)return
        if(listeners.containsKey(urlString)){
            listeners.get(urlString)?.remove(listener)
        }
    }

    private inner class DownloadSubscribe : ObservableOnSubscribe<DownloadInfo> {
        private var downloadInfo: DownloadInfo? = null

        constructor(downloadInfo:DownloadInfo){
            this.downloadInfo = downloadInfo
            getDownloadInfo(downloadInfo)
        }

        override fun subscribe(e: ObservableEmitter<DownloadInfo>) {
            Log.e("DownloadSubscribe","subscribe")
            download(downloadInfo,e)
        }

        private fun download(info: DownloadInfo?,e: ObservableEmitter<DownloadInfo>) {
            Log.e("DownloadSubscribe","DownloadSubscribe")
            if(info == null)return
            var link = info.url
            var downloadedLength = info.progress
            var request: Request = Request.Builder()
                    .addHeader("RANGE","bytes=" + downloadedLength + "-")
                    .url(link)
                    .build()
            var call = client!!.newCall(request)
            downCalls.put(link,call)
            var response = call.execute()

            var file = File(info.dir,info.fileName)
            var input: InputStream? = null
            var saveFile: RandomAccessFile? = null
            try {
                saveFile = RandomAccessFile(file, "rw")
                saveFile.seek(info.progress)
                if (response == null || !response.isSuccessful) return
                input = response.body()!!.byteStream()
                var buffer: ByteArray = ByteArray(2048)
                //kotlin 中等式(赋值)不是一个表达式
                while ((input.read(buffer)) != -1) {
                    Log.e("DownloadSubscribe","while循环")
                    info.progress += buffer.size
                    saveFile.write(buffer, 0, buffer.size)
                    //这里传入整个downloadInfo链
                    if(e==null) Log.e("DownloadSubscribe","e 为空")

                    e.onNext(downloadInfo!!)
                }
                downCalls.remove(link)
            }finally{
                if(input!=null) {
                    input.close()
                }
                if(saveFile!=null){
                    saveFile.close()
                }
                response.body()?.close()
            }
            if(info.internal!=null)download(info.internal,e)
            e.onComplete()
        }
    }

    interface DownloadListener{
        fun onNext(downloadInfo: DownloadInfo)
        fun onComplete(downloadInfo: DownloadInfo?)
    }
}

这里的核心是利用rxjava的线程调度和okhttp的请求来完成文件的下载。这里有一个问题是我想要让歌曲和歌词的进度合并在一起,但是他们又是两个不同的url来下载的,所以是两个downloadInfo对象,最终用装饰者模式解决。看一下downloadInfo类:

class DownloadInfo {

    companion object {
        final val TOTAL_ERROR: Long = -1
    }

    var internal: DownloadInfo? = null
    var url: String = ""
    var total: Long = -1
    var progress: Long = 0
    var dir: String = ""
    var fileName: String = ""

    constructor(url:String, internal: DownloadInfo?){
        this.url = url
        this.internal = internal
    }

    fun getDown():Long{
        var interDown:Long = 0
        if(internal!=null)interDown = internal!!.getDown()
        return progress+interDown
    }
}

这样就能将歌曲和歌词的进度合并在一起并返回给listener了。

显示进度的可以直接注册listener,在onNext会调用响应的listener的方法返回进度信息,这里就不展示了。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值