Android okhttp+rxjava實現多檔案下載和斷點續傳

TTMMJJ99發表於2017-11-10

轉載地址:http://blog.csdn.net/bit_kaki/article/details/72566020?locationNum=3&fps=1

      先說下我的需求。我的需求是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)

相關文章