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

    先说下我的需求。我的需求是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)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值