Android OkHttp 下载多个文件 断点下载
demo的主要逻辑是,利用okhttp 和 RxJava 在子线程中下载文件,通关观察者模式监听下载的进度,再回调到主线程中,然后利用EventBus 通知页面刷新,更新进度。
// OKHttp RxJava
implementation 'com.squareup.okhttp3:okhttp:3.6.0'
implementation 'io.reactivex.rxjava2:rxjava:2.1.3'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
// eventbus
implementation 'org.greenrobot:eventbus:3.0.0'
implementation 'com.android.support:recyclerview-v7:27.1.1'
/**
* Created by zs
* Date:2018年 09月 12日
* Time:13:50
* —————————————————————————————————————
* About: 下载管理
* —————————————————————————————————————
*/
public class DownloadInfo {
/**
* 下载状态
*/
public static final String DOWNLOAD = "download";
public static final String DOWNLOAD_PAUSE = "pause";
public static final String DOWNLOAD_CANCEL = "cancel";
public static final String DOWNLOAD_OVER = "over";
public static final String DOWNLOAD_ERROR = "error";
public static final long TOTAL_ERROR = -1;//获取进度失败
private String url;
private String fileName;
private String downloadStatus;
private long total;
private long progress;
public DownloadInfo(String url) {
this.url = url;
}
public String getUrl() {
return url;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
public long getProgress() {
return progress;
}
public void setProgress(long progress) {
this.progress = progress;
}
public String getDownloadStatus() {
return downloadStatus;
}
public void setDownloadStatus(String downloadStatus) {
this.downloadStatus = downloadStatus;
}
}
/**
* Created by zs
* Date:2018年 09月 12日
* Time:13:56
* —————————————————————————————————————
* About: 下载管理
* —————————————————————————————————————
*/
public class DownloadManager {
private static final AtomicReference<DownloadManager> INSTANCE = new AtomicReference<>();
private OkHttpClient mClient;
private HashMap<String, Call> downCalls; //用来存放各个下载的请求
public static DownloadManager getInstance() {
for (; ; ) {
DownloadManager current = INSTANCE.get();
if (current != null) {
return current;
}
current = new DownloadManager();
if (INSTANCE.compareAndSet(null, current)) {
return current;
}
}
}
private DownloadManager() {
downCalls = new HashMap<>();
mClient = new OkHttpClient.Builder().build();
}
/**
* 查看是否在下载任务中
* @param url
* @return
*/
public boolean getDownloadUrl(String url){
return downCalls.containsKey(url);
}
/**
* 开始下载
*
* @param url 下载请求的网址
* @param downLoadObserver 用来回调的接口
*/
public void download(String url, DownloadObserver downLoadObserver) {
Observable.just(url)
.filter(new Predicate<String>() {
@Override
public boolean test(String s) {
return !downCalls.containsKey(s);
}
}) // 过滤 call的map中已经有了,就证明正在下载,则这次不下载
.flatMap(new Function<String, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(String s) {
return Observable.just(createDownInfo(s));
}
}) // 生成 DownloadInfo
.map(new Function<Object, DownloadInfo>() {
@Override
public DownloadInfo apply(Object o) {
return getRealFileName((DownloadInfo)o);
}
}) // 如果已经下载,重新命名
.flatMap(new Function<DownloadInfo, ObservableSource<DownloadInfo>>() {
@Override
public ObservableSource<DownloadInfo> apply(DownloadInfo downloadInfo) {
return Observable.create(new DownloadSubscribe(downloadInfo));
}
}) // 下载
.observeOn(AndroidSchedulers.mainThread()) // 在主线程中回调
.subscribeOn(Schedulers.io()) // 在子线程中执行
.subscribe(downLoadObserver); // 添加观察者,监听下载进度
}
/**
* 下载取消或者暂停
* @param url
*/
public void pauseDownload(String url) {
Call call = downCalls.get(url);
if (call != null) {
call.cancel();//取消
}
downCalls.remove(url);
}
/**
* 取消下载 删除本地文件
* @param info
*/
public void cancelDownload(DownloadInfo info){
pauseDownload(info.getUrl());
info.setProgress(0);
info.setDownloadStatus(DownloadInfo.DOWNLOAD_CANCEL);
EventBus.getDefault().post(info);
Constant.deleteFile(info.getFileName());
}
/**
* 创建DownInfo
*
* @param url 请求网址
* @return DownInfo
*/
private DownloadInfo createDownInfo(String url) {
DownloadInfo downloadInfo = new DownloadInfo(url);
long contentLength = getContentLength(url);//获得文件大小
downloadInfo.setTotal(contentLength);
String fileName = url.substring(url.lastIndexOf("/"));
downloadInfo.setFileName(fileName);
return downloadInfo;
}
/**
* 如果文件已下载重新命名新文件名
* @param downloadInfo
* @return
*/
private DownloadInfo getRealFileName(DownloadInfo downloadInfo) {
String fileName = downloadInfo.getFileName();
long downloadLength = 0, contentLength = downloadInfo.getTotal();
File path = new File(Constant.FILE_PATH);
if (!path.exists()) {
path.mkdir();
}
File file = new File(Constant.FILE_PATH, fileName);
if (file.exists()) {
//找到了文件,代表已经下载过,则获取其长度
downloadLength = file.length();
}
//之前下载过,需要重新来一个文件
int i = 1;
while (downloadLength >= contentLength) {
int dotIndex = fileName.lastIndexOf(".");
String fileNameOther;
if (dotIndex == -1) {
fileNameOther = fileName + "(" + i + ")";
} else {
fileNameOther = fileName.substring(0, dotIndex)
+ "(" + i + ")" + fileName.substring(dotIndex);
}
File newFile = new File(Constant.FILE_PATH, fileNameOther);
file = newFile;
downloadLength = newFile.length();
i++;
}
//设置改变过的文件名/大小
downloadInfo.setProgress(downloadLength);
downloadInfo.setFileName(file.getName());
return downloadInfo;
}
private class DownloadSubscribe implements ObservableOnSubscribe<DownloadInfo> {
private DownloadInfo downloadInfo;
public DownloadSubscribe(DownloadInfo downloadInfo) {
this.downloadInfo = downloadInfo;
}
@Override
public void subscribe(ObservableEmitter<DownloadInfo> e) throws Exception {
String url = downloadInfo.getUrl();
long downloadLength = downloadInfo.getProgress();//已经下载好的长度
long contentLength = downloadInfo.getTotal();//文件的总长度
//初始进度信息
e.onNext(downloadInfo);
Request request = new Request.Builder()
//确定下载的范围,添加此头,则服务器就可以跳过已经下载好的部分
.addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)
.url(url)
.build();
Call call = mClient.newCall(request);
downCalls.put(url, call);//把这个添加到call里,方便取消
Response response = call.execute();
File file = new File(Constant.FILE_PATH, downloadInfo.getFileName());
InputStream is = null;
FileOutputStream fileOutputStream = null;
try {
is = response.body().byteStream();
fileOutputStream = new FileOutputStream(file, true);
byte[] buffer = new byte[2048];//缓冲数组2kB
int len;
while ((len = is.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, len);
downloadLength += len;
downloadInfo.setProgress(downloadLength);
e.onNext(downloadInfo);
}
fileOutputStream.flush();
downCalls.remove(url);
} finally {
//关闭IO流
DownloadIO.closeAll(is, fileOutputStream);
}
e.onComplete();//完成
}
}
/**
* 获取下载长度
*
* @param downloadUrl
* @return
*/
private long getContentLength(String downloadUrl) {
Request request = new Request.Builder()
.url(downloadUrl)
.build();
try {
Response response = mClient.newCall(request).execute();
if (response != null && response.isSuccessful()) {
long contentLength = response.body().contentLength();
response.close();
return contentLength == 0 ? DownloadInfo.TOTAL_ERROR : contentLength;
}
} catch (IOException e) {
e.printStackTrace();
}
return DownloadInfo.TOTAL_ERROR;
}
}
这里要多分析一下,下载管理类是单例模式这是必须的,里面定义一个HashMap用来存放所有的下载任务,里面的download()方法,是主要过程
public void download(String url, DownloadObserver downLoadObserver) {
Observable.just(url)
.filter(new Predicate<String>() {
@Override
public boolean test(String s) {
return !downCalls.containsKey(s);
}
}) // 过滤 call的map中已经有了,就证明正在下载,则这次不下载
.flatMap(new Function<String, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(String s) {
return Observable.just(createDownInfo(s));
}
}) // 生成 DownloadInfo
.map(new Function<Object, DownloadInfo>() {
@Override
public DownloadInfo apply(Object o) {
return getRealFileName((DownloadInfo)o);
}
}) // 如果已经下载,重新命名
.flatMap(new Function<DownloadInfo, ObservableSource<DownloadInfo>>() {
@Override
public ObservableSource<DownloadInfo> apply(DownloadInfo downloadInfo) {
return Observable.create(new DownloadSubscribe(downloadInfo));
}
}) // 下载
.observeOn(AndroidSchedulers.mainThread()) // 在主线程中回调
.subscribeOn(Schedulers.io()) // 在子线程中执行
.subscribe(downLoadObserver); // 添加观察者,监听下载进度
}
这里用到的是Rxjava,第一步filter,过滤下载,已经下载的url再点击下载是不会另起下载任务的;第二步flatMap,通过url生成Bean类,这个按需求来设计,也可以直接传一个Bean进来也是可以的;第三步map,如果这个文件已经下载了,再次下载重新命名文件,这也是根据需求改变,如果下载过的文件不需要下载,这就可以省了;第四步flatMap,去下载具体去看下载方法;剩下的就是切换线程和添加观察者了。
/**
* Created by zs
* Date:2018年 09月 12日
* Time:13:50
* —————————————————————————————————————
* About: 观察者
* —————————————————————————————————————
*/
public class DownloadObserver implements Observer<DownloadInfo> {
public Disposable d;//可以用于取消注册的监听者
public DownloadInfo downloadInfo;
@Override
public void onSubscribe(Disposable d) {
this.d = d;
}
@Override
public void onNext(DownloadInfo value) {
this.downloadInfo = value;
downloadInfo.setDownloadStatus(DownloadInfo.DOWNLOAD);
EventBus.getDefault().post(downloadInfo);
}
@Override
public void onError(Throwable e) {
Log.d("My_Log","onError");
if (DownloadManager.getInstance().getDownloadUrl(downloadInfo.getUrl())){
DownloadManager.getInstance().pauseDownload(downloadInfo.getUrl());
downloadInfo.setDownloadStatus(DownloadInfo.DOWNLOAD_ERROR);
EventBus.getDefault().post(downloadInfo);
}else{
downloadInfo.setDownloadStatus(DownloadInfo.DOWNLOAD_PAUSE);
EventBus.getDefault().post(downloadInfo);
}
}
@Override
public void onComplete() {
Log.d("My_Log","onComplete");
if (downloadInfo != null){
downloadInfo.setDownloadStatus(DownloadInfo.DOWNLOAD_OVER);
EventBus.getDefault().post(downloadInfo);
}
}
}
这里是用到EventBus来通知页面刷新的,当然也可以不用,把订阅者直接写在Activity,但是那样不利于代码的复用,如果多个页面需要进度更新就麻烦了。
页面中是一个RecyclerView,交互逻辑不是很多,说一点,Adapter条目的更新,用的notifyItemChanged(i) 每次更新某一条的进度,而不是notifyDataSetChanged()全部刷新,因为调用全部刷新,刷新的频率很高会导致条目中控件的点击事件不好用没有响应,被拦截了,而更新某一条也会有一个问题是条目刷新时会闪动,解决方案是把RecyclerView的刷新动画去掉,这样就解决了。
// 取消item刷新的动画
((SimpleItemAnimator)recycler_view.getItemAnimator()).setSupportsChangeAnimations(false);
/**
* Created by zs
* Date:2018年 09月 11日
* Time:18:06
* —————————————————————————————————————
* About:
* —————————————————————————————————————
*/
public class DownloadAdapter extends RecyclerView.Adapter<DownloadAdapter.UploadHolder> {
private List<DownloadInfo> mdata;
public DownloadAdapter(List<DownloadInfo> mdata) {
this.mdata = mdata;
}
/**
* 更新下载进度
* @param info
*/
public void updateProgress(DownloadInfo info){
for (int i = 0; i < mdata.size(); i++){
if (mdata.get(i).getUrl().equals(info.getUrl())){
mdata.set(i,info);
notifyItemChanged(i);
break;
}
}
}
@Override
public UploadHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = View.inflate(parent.getContext(), R.layout.item_download_layout,null);
return new UploadHolder(view);
}
@Override
public void onBindViewHolder(UploadHolder holder, int position) {
final DownloadInfo info = mdata.get(position);
if (DownloadInfo.DOWNLOAD_CANCEL.equals(info.getDownloadStatus())){
holder.main_progress.setProgress(0);
}else if (DownloadInfo.DOWNLOAD_OVER.equals(info.getDownloadStatus())){
holder.main_progress.setProgress(holder.main_progress.getMax());
}else {
if (info.getTotal() == 0){
holder.main_progress.setProgress(0);
}else {
float d = info.getProgress() * holder.main_progress.getMax() / info.getTotal();
holder.main_progress.setProgress((int) d);
}
}
holder.main_btn_down.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DownloadManager.getInstance().download(info.getUrl(), new DownloadObserver());
}
});
holder.main_btn_pause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DownloadManager.getInstance().pauseDownload(info.getUrl());
}
});
holder.main_btn_cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
DownloadManager.getInstance().cancelDownload(info);
}
});
}
@Override
public int getItemCount() {
return mdata.size();
}
public class UploadHolder extends RecyclerView.ViewHolder{
private ProgressBar main_progress;
private Button main_btn_down;
private Button main_btn_pause;
private Button main_btn_cancel;
public UploadHolder(View itemView) {
super(itemView);
main_progress = itemView.findViewById(R.id.main_progress);
main_btn_down = itemView.findViewById(R.id.main_btn_down);
main_btn_pause = itemView.findViewById(R.id.main_btn_pause);
main_btn_cancel = itemView.findViewById(R.id.main_btn_cancel);
}
}
}
https://github.com/QQzs/DownloadFile
https://www.jianshu.com/p/4bab2e9c577e
参考地址: https://blog.csdn.net/cfy137000/article/details/54838608 所有内容基本是参考的这篇文章,我只是少有修改,希望给大家有所帮助。