最近在音乐app中遇到在线歌曲下载问题,于是有了这篇文章。这篇文章借鉴了https://blog.csdn.net/cfy137000/article/details/54838608,在此感谢,自己在此基础上修改了一点。
代码用Kotlin写的,这里只上核心代码。
class DownloadManager {
//这里采用单例模式
companion object {
private val INSTANCE:AtomicReference = 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 = HashMap()
var client: OkHttpClient? = null //全局使用同一个OkHttpClient
//哈希表存放监听者,以URL为key,ArrayList允许存在想要监听同一个URL的Listener,相当于观察者模式
var listeners: HashMap> = 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 {
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()
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 {
private var downloadInfo: DownloadInfo? = null
constructor(downloadInfo:DownloadInfo){
this.downloadInfo = downloadInfo
getDownloadInfo(downloadInfo)
}
override fun subscribe(e: ObservableEmitter) {
Log.e("DownloadSubscribe","subscribe")
download(downloadInfo,e)
}
private fun download(info: DownloadInfo?,e: ObservableEmitter) {
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的方法返回进度信息,这里就不展示了。