您的位置:首页 > 理论基础 > 计算机网络

Android okhttp+rxjava实现多文件下载和断点续传

2017-05-19 21:50 513 查看
         首先先感谢丰神,核心代码源于他的这篇微博http://blog.csdn.net/cfy137000/article/details/54838608,思路很棒。基于他的代码做了一些改动,实现我所需功能。

      

      先说下我的需求。我的需求是PC端先进行更新数据的管理,然后移动端登录时候会自动访问服务,传入mac值,获取需更新数据的信息。如下图所示:



      

        从服务返回到的是json格式的字符串,我解析后获得一个list<bean>,bean的结构为:

public class OfflineDataBean {

private String dataId;
private String dataName;
private String organizationName;
private String mac;
private int dataType;
private String dataAddtime;
private String dataUpdatetime;
private String dataPath;
private String dataStatus;
private String remark;

...
}

      

      接下来就是将这个list展示在一个RecyclerView里。在这里我首先将RecyclerView的Adapter和Holder进行了一次封装:

public abstract class BaseRecyclerAdapter<T> extends RecyclerView.Adapter<RecyclerViewHolder> {
//list集合
protected final List<T> mData;
protected final Context mContext;
//上下文
protected LayoutInflater mInflater;
//点击item监听
private OnItemClickListener mClickListener;
//长按item监听
private OnItemLongClickListener mLongClickListener;

/**
* 构造方法
*
* @param ctx
* @param list
*/
public BaseRecyclerAdapter(Context ctx, List<T> list) {
mData = (list != null) ? list : new ArrayList<T>();
mContext = ctx;
mInflater = LayoutInflater.from(ctx);
}

public void clear() {
this.mData.clear();
}

/**
* 方法中主要是引入xml布局文件,并且给item点击事件和item长按事件赋值
*
* @param parent
* @param viewType
* @return
*/
@Override
public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, final int viewType) {
final RecyclerViewHolder holder = new RecyclerViewHolder(mContext,
mInflater.inflate(getItemLayoutId(viewType), parent, false));
if (mClickListener != null) {
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mClickListener.onItemClick(holder.itemView, holder.getPosition());
}
});
}
if (mLongClickListener != null) {
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
mLongClickListener.onItemLongClick(holder.itemView, holder.getPosition());
return true;
}
});
}
return holder;
}

/**
* onBindViewHolder这个方法主要是给子项赋值数据的
*
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(RecyclerViewHolder holder, int position) {
bindData(holder, position, mData.get(position));
}

@Override
public int getItemCount() {
return mData.size();
}

/**
* add方法是添加item方法
*
* @param pos
* @param item
*/
public void add(int pos, T item) {
mData.add(pos, item);
notifyItemInserted(pos);
}

/**
* delete方法是删除item方法
*
* @param pos
*/
public void delete(int pos) {
mData.remove(pos);
notifyItemRemoved(pos);
}

/**
* item点击事件set方法
*
* @param listener
*/
public void setOnItemClickListener(OnItemClickListener listener) {
mClickListener = listener;
}

/**
* item长安事件set方法
*
* @param listener
*/
public void setOnItemLongClickListener(OnItemLongClickListener listener) {
mLongClickListener = listener;
}

/**
* item中xml布局文件方法
*
* @param viewType
* @return
*/
abstract public int getItemLayoutId(int viewType);

/**
* 赋值数据方法
*
* @param holder
* @param position
* @param item
*/
abstract public void bindData(RecyclerViewHolder holder, int position, T item);

/**
* item点击事件接口
*/
public interface OnItemClickListener {
public void onItemClick(View itemView, int pos);
}

/**
* item长按事件接口
*/
public interface OnItemLongClickListener {
public void onItemLongClick(View itemView, int pos);
}
}

public class RecyclerViewHolder extends RecyclerView.ViewHolder {
/**
* 集合类,layout里包含的View,以view的id作为key,value是view对象
*/
private SparseArray<View> mViews;
/**
* 上下文对象
*/
private Context mContext;

/**
* 构造方法
*
* @param ctx
* @param itemView
*/
public RecyclerViewHolder(Context ctx, View itemView) {
super(itemView);
mContext = ctx;
mViews = new SparseArray<View>();
}

/**
* 存放xml页面方法
*
* @param viewId
* @param <T>
* @return
*/
private <T extends View> T findViewById(int viewId) {
View view = mViews.get(viewId);
if (view == null) {
view = itemView.findViewById(viewId);
mViews.put(viewId, view);
}
return (T) view;
}

public View getView(int viewId) {
return findViewById(viewId);
}

/**
* 存放文本的id
*
* @param viewId
* @return
*/
public TextView getTextView(int viewId) {
return (TextView) getView(viewId);
}

/**
* 存放button的id
*
* @param viewId
* @return
*/
public Button getButton(int viewId) {
return (Button) getView(viewId);
}

/**
* 存放图片的id
*
* @param viewId
* @return
*/
public ImageView getImageView(int viewId) {
return (ImageView) getView(viewId);
}
public LinearLayout getLinearLayout(int viewId) {
return (LinearLayout) getView(viewId);
}
public ProgressBar getProgressBar(int viewId)  {
return (ProgressBar) getView(viewId);
}

/**
* 存放图片按钮的id
*
* @param viewId
* @return
*/
public ImageButton getImageButton(int viewId) {
return (ImageButton) getView(viewId);
}

/**
* 存放输入框的id
*
* @param viewId
* @return
*/
public EditText getEditText(int viewId) {
return (EditText) getView(viewId);
}

/**
* 存放文本xml中的id并且可以赋值数据的方法
*
* @param viewId
* @param value
* @return
*/
public RecyclerViewHolder setText(int viewId, String value) {
TextView view = findViewById(viewId);
view.setText(value);
return null;
}

/**
* 存放图片xml中的id并且可以赋值数据的方法
*
* @param viewId
* @param resId
* @return
*/
public RecyclerViewHolder setBackground(int viewId, int resId) {
View view = findViewById(viewId);
view.setBackgroundColor(resId);
return null;
}

/**
* 存放点击事件监听
*
* @param viewId
* @param listener
* @return
*/
public RecyclerViewHolder setClickListener(int viewId, View.OnClickListener listener) {
View view = findViewById(viewId);
view.setOnClickListener(listener);
return null;
}
}

   

      然后RecyclerView里的item布局文件为:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="horizontal"
android:paddingLeft="10dp"
android:layout_height="60dp">

<TextView
android:id="@+id/tv_name"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:textSize="16sp"/>

<ProgressBar
android:id="@+id/main_progress"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
style="@style/Widget.AppCompat.ProgressBar.Horizontal" />

<TextView
android:id="@+id/tv_percent"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00"
android:textSize="18sp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="/100"
android:textSize="18sp"/>
<Button
android:id="@+id/btn_down"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始下载"/>

</LinearLayout>


      开始布局RecyclerView,思路为点击开始下载按钮,开始下载文件,再次点击暂停下载并可以续传下载。下载完毕后提示下载完毕。

baseRecyclerAdapterOfflineData=new BaseRecyclerAdapter<OfflineDataBean>(this,offlineDataBeenList) {
@Override
public int getItemLayoutId(int viewType) {
return R.layout.item_offlinedata;
}

@Override
public void bindData(RecyclerViewHolder holder, int position, OfflineDataBean item) {
TextView tvName=holder.getTextView(R.id.tv_name);
TextView tvpercent=holder.getTextView(R.id.tv_percent);
Button btnDown=holder.getButton(R.id.btn_down);
ProgressBar progressBar=holder.getProgressBar(R.id.main_progress);
tvName.setText(offlineDataBeenList.get(position).getDataName());
btnDown.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(btnDown.getText().equals("开始下载")||btnDown.getText().equals("继续下载")) {
DownloadManager.getInstance().download(offlineDataBeenList.get(position).getDataPath(), new DownLoadObserver() {
@Override
public void onNext(DownloadInfo value) {
super.onNext(value);
tvpercent.setText(String.valueOf((int)(((double)value.getProgress()/(double)value.getTotal())*100.00)));
progressBar.setMax((int) value.getTotal());
progressBar.setProgress((int) value.getProgress());
btnDown.setText("暂停下载");
}

@Override
public void onComplete() {
if (downloadInfo != null) {
btnDown.setText("下载结束");
}
}
});
}else if(btnDown.getText().toString().equals("暂停下载")) {
DownloadManager.getInstance().cancel(offlineDataBeenList.get(position).getDataPath());
btnDown.setText("开始下载");
}
}
});
}

};

rvDownload.setAdapter(baseRecyclerAdapterOfflineData);
rvDownload.setLayoutManager(new LinearLayoutManager(this));
rvDownload.setItemAnimator(new DefaultItemAnimator());


      重点还是在DownloadManager类,再次感谢下丰神。这个类里最重要的是download方法,如下所示:

public void download(String url, DownLoadObserver downLoadObserver) {
Observable.just(url)
.filter(s -> !downCalls.containsKey(s))//call的map已经有了,就证明正在下载,则这次不下载
.flatMap(s -> Observable.just(createDownInfo(s)))
.map(this::getRealFileName)//检测本地文件夹,生成新的文件名
.flatMap(downloadInfo -> Observable.create(new DownloadSubscribe(downloadInfo)))//下载
.observeOn(AndroidSchedulers.mainThread())//在主线程回调
.subscribeOn(Schedulers.io())//在子线程执行
.subscribe(downLoadObserver);//添加观察者

}


      其中url是文件下载地址,downloadObserver是用来回调的接口,监听下载情况。简要说明下这个rxjava的方法,从上往下每行的意思分别是:

      传入url参数;

      判断是否正在这个url下载文件,如果存在,则这次不下载(防止多次点击同一个下载按钮);

      获取并传入下载信息;

      检测本地文件(文件是否存在,如果存在已下载多少);

      根据下载信息创建下载的观察者方法;

      设置在主线程回调;

      观察者方法在子线程执行;

      添加观察者方法,开始执行。

      

      附上完整代码:

public class DownloadManager {

private static final AtomicReference<DownloadManager> INSTANCE = new AtomicReference<>();
private HashMap<String, Call> downCalls;//用来存放各个下载的请求
private OkHttpClient mClient;//OKHttpClient;

//获得一个单例类
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 下载请求的网址
* @param downLoadObserver 用来回调的接口
*/
public void download(String url, DownLoadObserver downLoadObserver) { Observable.just(url) .filter(s -> !downCalls.containsKey(s))//call的map已经有了,就证明正在下载,则这次不下载 .flatMap(s -> Observable.just(createDownInfo(s))) .map(this::getRealFileName)//检测本地文件夹,生成新的文件名 .flatMap(downloadInfo -> Observable.create(new DownloadSubscribe(downloadInfo)))//下载 .observeOn(AndroidSchedulers.mainThread())//在主线程回调 .subscribeOn(Schedulers.io())//在子线程执行 .subscribe(downLoadObserver);//添加观察者 }

public void cancel(String url) {
Call call = downCalls.get(url);
if (call != null) {
call.cancel();//取消
}
downCalls.remove(url);
}

/**
* 创建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;
}

private DownloadInfo getRealFileName(DownloadInfo downloadInfo) {
String fileName = downloadInfo.getFileName();
long downloadLength = 0, contentLength = downloadInfo.getTotal();
File file = new File(MyApp.sContext.getFilesDir(), 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(MyApp.sContext.getFilesDir(), 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(MyApp.sContext.getFilesDir(), 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流
IOUtil.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;
}

}


      其他地方不用多说,最核心一句代码是:

.addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)

      通过这行代码确定下载的范围,从已下载的地方下载到结束。前面在创建被观察者时候执行的两个方法createDownInfo和getRealFileName就是为了分别获取总长度和已下载长度。引用丰神博文原话来说就是:

当要断点续传的话必须添加这个头,让输入流跳过多少字节的形式是不行的,所以我们要想能成功的添加这条信息那么就必须对这个url请求2次,一次拿到总长度,来方便判断本地是否有下载一半的数据,第二次才开始真正的读流进行网络请求,我还想了一种思路,当文件没有下载完成的时候添加一个自定义的后缀,当下载完成再把这个后缀取消了,应该就不需要请求两次了

      对应的下载信息DownloadInfo为:

public class DownloadInfo {
public static final long TOTAL_ERROR = -1;//获取进度失败
private String url;
private long total;
private long progress;
private String fileName;

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  abstract class DownLoadObserver implements Observer<DownloadInfo> {
protected Disposable d;//可以用于取消注册的监听者
protected DownloadInfo downloadInfo;
@Override
public void onSubscribe(Disposable d) {
this.d = d;
}

@Override
public void onNext(DownloadInfo downloadInfo) {
this.downloadInfo = downloadInfo;
}

@Override
public void onError(Throwable e) {
e.printStackTrace();
}
}


      主要代码已经贴上,让我们来看看效果为:



      可以断点续传,可以监听到实时下载情况,可以同时多个下传。

      需求达成。

.addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: