Android 基於OkHttp的下載,支持https,斷點下載,優化下載速度

package com.lenovo.smarthcc.http.okhttp;

import android.text.TextUtils;

import com.lenovo.smarthcc.http.Listenner;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import okhttp3.Call;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okio.Buffer;
import okio.Okio;
import okio.Sink;
import okio.Source;

/**
 * <p>Author:    shijiale</p>
 * <p>Date:      2017/3/14.<p>

 * <p>Describe:
 */

public class BRDownloadThread extends Thread {

    private static OkHttpClient mClient;                 //okhttp client
    private Builder mBuilder;                            //配置
    private boolean isExit = false;                      //是否退出下載
    private long mStartTime = 0;                         //當前這次下載起始時間
    private long mDownloadLength;                        //當前這次下載的長度
    private final int MAX_BUFF_SIZE = 4 * 1024;          //每次發送的字節(這個數值可能沒有什麼作用,因爲大部分http流都會做緩存)
    private Call mCall;                                  //當前請求
    private DaemonThead mDaemonMsgThread;                //用於進度回調的線程

    /***
     * 退出當前下載線程
     */
    public synchronized void exit() {
        isExit = true;
    }


    private BRDownloadThread() {
        mClient = OkHttpProxy.getClient();
        if (mDaemonMsgThread == null) {
            mDaemonMsgThread = new DaemonThead();
            mDaemonMsgThread.isRunning = true;
        }
    }

    @Override
    public void run() {
        super.run();
        try {
            downloadFile();
        } catch (Exception e) {
            mBuilder.mErrorListenner.onChanged(Listenner.STATUS_ERROR, mBuilder.mSavePath, e.getMessage());
        } finally {
            if (mDaemonMsgThread != null) {
                mDaemonMsgThread.isRunning = false;
                mDaemonMsgThread = null;
            }
        }
    }

    /***
     * 自動修改下載的文件名稱
     * @param fileLength 文件在服務器的大小
     */
    private void autoChangeFileName(long fileLength) {
        File file = new File(mBuilder.mSavePath);
        if(file != null && file.exists() && file.length() == fileLength) {
            String path = mBuilder.mSavePath;
            path = path.substring(0,path.lastIndexOf(".") + 1);

            String expresion = mBuilder.mSavePath.substring(path.lastIndexOf(".") + 1,mBuilder.mSavePath.length());
            path += "(" + new Date(System.currentTimeMillis()).toLocaleString() + ")" + expresion;
            mBuilder.mSavePath = path;
        }
    }

    private boolean chekDirectory() {
        String path = mBuilder.mSavePath;
        path = path.substring(0,path.lastIndexOf(File.separator) + 1);
        File file = new File(path);
        if(file != null && file.exists()) {
            return true;
        }
        if(file == null || !file.exists()) {
            boolean mkdirs = file.mkdirs();
            if(!mkdirs) {
                return false;
            }
        }
        return true;
    }

    /***
     * 下載文件
     * @throws IOException
     */
    private void downloadFile() throws IOException {

        if(!chekDirectory()) {
            mBuilder.mErrorListenner.onChanged(Listenner.STATUS_ERROR,mBuilder.mSavePath,"directory create failed!");
            return;
        }
        //獲取文件長度
        mBuilder.mFileAllLength = getNetFileSize();
        //如果允許重複下載,則對文件進行重命名,修改後的文件名會會在 onStop onSuccess中體現
        if(mBuilder.isAutoRename) {
            autoChangeFileName(mBuilder.mFileAllLength);
        }
        RandomAccessFile file = new RandomAccessFile(mBuilder.mSavePath, "rw");
        //如果本地已經存在該文件,並且大小和服務器相同則返回下載成功
        if (file != null && file.length() == mBuilder.mFileAllLength && mBuilder.mFileAllLength != 0) {
            mBuilder.mSuccessListenner.onChanged(Listenner.STATUS_SUCCESS, mBuilder.mSavePath,mBuilder.mFileAllLength + "");
            return;
        }
        //設置文件起始指針
        if (file != null && file.length() != 0) {
            if (mBuilder.mStartOffset > 0 && mBuilder.mStartOffset < file.length()) {
                file.seek(mBuilder.mStartOffset);
            } else {
                file.seek(file.length() - 1);
                mBuilder.mStartOffset = file.length() - 1;
            }
        }
        //設置請求參數
        callRequest(mBuilder.mStartOffset);
        if (mCall == null) {
            mBuilder.mErrorListenner.onChanged(Listenner.STATUS_ERROR, mBuilder.mSavePath, "CONNECTION ERROR!");
            return;
        }
        //建立連接
        Response res = mCall.execute();
        if (!res.isSuccessful()) {
            mBuilder.mErrorListenner.onChanged(Listenner.STATUS_ERROR, mBuilder.mSavePath, "CONNECTION ERROR!");
            return;
        }
        //更新文件長度
        mBuilder.mLengthChangeListenner.onChanged(Listenner.STATUS_LENGTH_CHANGE, mBuilder.mFileAllLength + "", (mBuilder.mStartOffset + mDownloadLength) + "");
        //開啓進度刷新線程
        mDaemonMsgThread.start();
        //保存文件
        boolean ret = saveData(res.body().byteStream(), new FileOutputStream(file.getFD()));
        if (ret) {
            mBuilder.mSuccessListenner.onChanged(Listenner.STATUS_SUCCESS, mBuilder.mSavePath,mBuilder.mFileAllLength + "");
        } else {
            mBuilder.mStopListenner.onChanged(Listenner.STATUS_STOP, mBuilder.mSavePath, mBuilder.mFileAllLength + "", (mBuilder.mStartOffset + mDownloadLength) + "");
        }
        mBuilder.mLengthChangeListenner.onChanged(Listenner.STATUS_LENGTH_CHANGE, mBuilder.mFileAllLength + "", (mBuilder.mStartOffset + mDownloadLength) + "");
    }

    /***
     * 獲取文件在服務器的長度,head請求
     * @return
     * @throws IOException
     */
    private long getNetFileSize() throws IOException {
        Request request = getRequestBuilder(0)
                .head()
                .build();
        Call call = mClient.newCall(request);
        if (call == null) {
            mBuilder.mErrorListenner.onChanged(Listenner.STATUS_ERROR, mBuilder.mSavePath, "CONNECTION ERROR,Get File Size!");
            return -1;
        }

        Response response = call.execute();
        if (!response.isSuccessful()) {
            mBuilder.mErrorListenner.onChanged(Listenner.STATUS_ERROR, mBuilder.mSavePath, "CONNECTION ERROR,Get File Size!");
            return -1;
        }

        Headers headers = response.headers();
        long fileSize = 0;
        try {
            fileSize = Long.parseLong(headers.get("Content-Length"));
        } catch (Exception e) {
            fileSize = -1;
        }
        return fileSize;
    }

    /***
     * 執行請求
     * @param offset 起始下載偏移量,該偏移量以當前已經下載的文件長度爲準
     */
    private void callRequest(long offset) {
        Request request = getRequestBuilder(offset)
                .build();
        mCall = mClient.newCall(request);
    }

    /***
     * 初始化請求參數
     * @param offset 起始下載偏移量
     * @return
     */
    private Request.Builder getRequestBuilder(long offset) {
        Headers.Builder builder = new Headers.Builder();
        //header
        if (mBuilder.mHeaders != null) {
            for (String k : mBuilder.mHeaders.keySet()) {
                builder.set(k, mBuilder.mHeaders.get(k));
            }
        }
        if (builder.get("Range") == null) {
            builder.set("Range", "bytes=" + offset + "-");
        }
        Headers headers = builder.build();
        //urlparams url後面的參數
        if (mBuilder.mUrlParams != null && !mBuilder.mUrlParams.isEmpty()) {
            if (!mBuilder.mUrl.contains("?")) {
                mBuilder.mUrl += "?";
            }
            for (String k : mBuilder.mUrlParams.keySet()) {
                mBuilder.mUrl += k + "=" + mBuilder.mUrlParams.get(k);
                mBuilder.mUrl += "&";
            }
        }
        Request.Builder reBuilder = new Request.Builder()
                .url(mBuilder.mUrl)
                .headers(headers);
        return reBuilder;
    }


    /***
     * 保存文件
     * @param is 源輸入流
     * @param os 目標輸出流
     * @return 如果是下載完成返回true,如果停止導致返回false
     * @throws IOException
     */
    private boolean saveData(InputStream is, OutputStream os) throws IOException {
        Source source = Okio.source(is);
        Sink sink = Okio.sink(os);
        Buffer buf = new Buffer();
        long len = 0;

        while ((len = source.read(buf, MAX_BUFF_SIZE)) != -1 && !isExit) {
            sink.write(buf, len);
            mDownloadLength += len;
        }

        sink.flush();
        sink.close();
        source.close();
        return !isExit;
    }

    /***
     * 刷新當前下載進度
     */
    private synchronized void changeSpeed() {
        long endTime = System.currentTimeMillis();
        mBuilder.mSpeedChangeListenner.onChanged(Listenner.STATUS_SPEED_CHANGE, mBuilder.mFileAllLength + "", (mBuilder.mStartOffset + mDownloadLength) + "",(endTime - mStartTime) + "", mDownloadLength + "");
    }

    /**
     * 用於在一個新的線程中刷新下載進度,防止回調中有耗時操作阻塞下載線程影響下載速度
     */
    private final class DaemonThead extends Thread {
        //是否開啓
        private boolean isRunning = true;

        @Override
        public void run() {
            while (isRunning) {
                //防止下載完成以及其他狀態回調時,進度還在刷新
                synchronized (mBuilder) {
                    changeSpeed();
                    try {
                        Thread.sleep(mBuilder.mSpeedRefreshHZ);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    /***
     * Listtenner 的簡單封裝,可以簡單的得到狀態回調中的數值
     */
    public static abstract class SimpleDownLoadListenner implements Listenner {

        @Override
        public void onChanged(int code, String... value) {
            switch (code) {
                case Listenner.STATUS_ERROR: {
                    onDownloadError(value[0], value[1]);
                }
                break;
                case Listenner.STATUS_SUCCESS: {
                    onDownloadSuccessful(value[0],getLongValue(value[1]));
                }
                break;
                case Listenner.STATUS_SPEED_CHANGE: {
                    onDownloadSpeedChanged(getLongValue(value[0]), getLongValue(value[1]), getLongValue(value[2]),getLongValue(value[3]));
                }
                break;
                case Listenner.STATUS_STOP: {
                    onDownloadStoped(value[0], getLongValue(value[1]), getLongValue(value[2]));
                }
                break;
                case Listenner.STATUS_LENGTH_CHANGE: {
                    onDownloadLengthChanged(getLongValue(value[0]), getLongValue(value[1]));
                }
                break;
            }
        }

        private long getLongValue(String value) {
            try {
                return Long.parseLong(value);
            } catch (Exception e) {
                return 0;
            }
        }

        public void onDownloadSuccessful(String path,long allFileLength) {

        }

        public void onDownloadStoped(String path, long allLength, long allDownloadlength) {

        }

        public void onDownloadError(String path, String msg) {

        }

        public void onDownloadLengthChanged(long allLength, long allDownloadLength) {

        }

        public synchronized void onDownloadSpeedChanged(long allLength,long allDownloadLenght , long times, long nowDownloadLength) {

        }
    }

    /***
     * BRDownloadThread以Builder模式創建,不允許new
     */
    public static class Builder implements Listenner {
        private Map<String, String> mUrlParams;        //url後的參數
        private Map<String, String> mHeaders;          //請求頭
        private long mFileAllLength;                   //文件總長度
        private long mStartOffset = 0;                 //起始下載偏移量
        private String mUrl;                           //請求地址
        private String mSavePath;                      //保存路徑(全路徑)
        private String mBasePath;                      //保存文件夾名稱
        private String mSaveName;                      //文件名稱
        private Listenner mSuccessListenner;           //成功回調
        private Listenner mErrorListenner;             //失敗回調
        private Listenner mLengthChangeListenner;      //文件長度變化回調(只在獲取到服務器文件大小時回調,以及線程終止後回調(包括完成和停止))
        private Listenner mSpeedChangeListenner;       //進度刷新 刷新頻率爲(1/mSpeedRefreshHZ)
        private Listenner mStopListenner;              //停止回調,只在下載手動終止時回調
        private long mSpeedRefreshHZ = 500;            //進度刷新間隔 毫秒
        private boolean isAutoRename = false;          //是否支持文件重命名,如果本地已存在 false 不再重複下載 true 重命名後繼續下載

        public Builder setAutoRename(boolean auto) {
            isAutoRename = auto;
            return this;
        }

        /***
         * 設置進度刷新間隔
         * @param times 間隔時間 ms
         * @return
         */
        public Builder setDaemonRefreshDelayTime(long times) {
            mSpeedRefreshHZ = times;
            return this;
        }

        /***
         * 設置保存的的全路徑
         * @param path
         * @return
         */
        public Builder setSavePath(String path) {
            mSavePath = path;
            return this;
        }

        /**
         * 設置保存文件夾名稱
         * @param basePath 文件夾名稱
         * @return
         */
        public Builder setBasePath(String basePath) {
            mBasePath = basePath;
            return this;
        }

        /***
         * 保存文件名
         * @param name 文件名
         * @return
         */
        public Builder setSaveName(String name) {
            mSaveName = name;
            return this;
        }

        /***
         * 請求地址
         * @param url
         */
        public Builder(String url) {
            mUrl = url;
        }

        public Builder() {

        }

        /***
         * 設置狀態監聽回調
         * @param l
         * @return
         */
        public Builder setListenners(Listenner l) {
            mSuccessListenner = l;
            mErrorListenner = l;
            mLengthChangeListenner = l;
            mSpeedChangeListenner = l;
            mStopListenner = l;
            return this;
        }

        /***
         * 成功監聽
         * @param l
         * @return
         */
        public Builder setSuccessListenner(Listenner l) {
            mSuccessListenner = l;
            return this;
        }

        /***
         * 失敗監聽
         * @param l
         * @return
         */
        public Builder setErrorListenner(Listenner l) {
            mErrorListenner = l;
            return this;
        }

        public Builder setStopListenner(Listenner l) {
            mStopListenner = l;
            return this;
        }

        /***
         * 長度變化監聽
         * @param l
         * @return
         */
        public Builder setLengthChangeListenner(Listenner l) {
            mLengthChangeListenner = l;
            return this;
        }

        /***
         * 進度刷新監聽
         * @param l
         * @return
         */
        public Builder setOnSpeedListenner(Listenner l) {
            mSpeedChangeListenner = l;
            return this;
        }

        /***
         * 設置起始下載偏移量和文件長度
         * @param offset 偏移量 首次下載爲 0,該值只作爲參考實際已文件長度爲準
         * @param fileAllLength 文件長度,可有可無
         * @return
         */
        public Builder setOffset(long offset, long fileAllLength) {
            mStartOffset = offset;
            mFileAllLength = fileAllLength;
            return this;
        }

        public Builder setUrl(String url) {
            mUrl = url;
            return this;
        }

        /***
         * 請求頭(如果header頭中包含Range 則會替換默認的Range屬性,建議不要包含)
         * @param headers
         * @return
         */
        public Builder setHeaders(Map<String, String> headers) {
            mHeaders = headers;
            return this;
        }

        /***
         * 請求頭
         * @param headers
         * @return
         */
        public synchronized Builder addHeaders(Map<String, String> headers) {
            if (mHeaders == null) {
                mHeaders = headers;
            } else {
                mHeaders.putAll(headers);
            }
            return this;
        }

        /***
         * 請求頭
         * @param k
         * @param v
         * @return
         */
        public synchronized Builder addHeader(String k, String v) {
            if (mHeaders == null) {
                mHeaders = new HashMap<>();
            }
            return this;
        }

        /***
         * 如果url後面包含參數,可以使用該方法爲url添加參數
         * @param params
         * @return
         */
        public synchronized Builder setUrlParams(Map<String, String> params) {
            mUrlParams = params;
            return this;
        }

        /***
         * url參數
         * @param params
         * @return
         */
        public synchronized Builder addUrlParams(Map<String, String> params) {
            if (mUrlParams == null) {
                mUrlParams = params;
            } else {
                mUrlParams.putAll(mUrlParams);
            }
            return this;
        }

        /**
         * url 參數
         * @param k
         * @param v
         * @return
         */
        public synchronized Builder addUrlParams(String k, String v) {
            if (mUrlParams == null) {
                mUrlParams = new HashMap<>();
            }
            mUrlParams.put(k, v);
            return this;
        }


        public BRDownloadThread build() throws RuntimeException {

            if (mErrorListenner == null) {
                mErrorListenner = this;
            }

            if (mSpeedChangeListenner == null) {
                mSpeedChangeListenner = this;
            }

            if (mSuccessListenner == null) {
                mSuccessListenner = this;
            }

            if (mStopListenner == null) {
                mStopListenner = this;
            }

            if (mLengthChangeListenner == null) {
                mLengthChangeListenner = this;
            }

            //文件保存路徑錯誤
            if (TextUtils.isEmpty(mSavePath) && (TextUtils.isEmpty(mBasePath)
                    || TextUtils.isEmpty(mSaveName))) {
                throw new RuntimeException("can not build BRDownloadthread,because savepath or (saveBasePasth and saveName is null)!");
            }

            //起始下載偏移量大於文件長度
            if (mStartOffset > mFileAllLength) {
                throw new RuntimeException("can not continue to download form startOffset,Because file length is null!");
            }

            if (TextUtils.isEmpty(mSavePath) && !TextUtils.isEmpty(mBasePath) && !TextUtils.isEmpty(mSaveName)) {
                mSavePath = mBasePath + File.separator + mSaveName;
            }

            BRDownloadThread thread = new BRDownloadThread();
            thread.mBuilder = this;
            return thread;
        }

        @Override
        public void onChanged(int code, String... value) {

        }
    }
}

示例:

 mDownloadThread = new BRDownloadThread.Builder(downloadPath)
                        .setSavePath(savePasth)
                        .setOffset(0,0)
                        .setListenners(new BRDownloadThread.SimpleDownLoadListenner() {
                            @Override
                            public void onChanged(int code, String... value) {
                                switch (code) {
                                    case Listenner.STATUS_ERROR: {
                                        LogUtil.i("ERROR == > " + getString(value));
                                    }break;
                                    case Listenner.STATUS_LENGTH_CHANGE: {
                                        LogUtil.i("LENGTH == >" + getString(value));
                                    }break;
                                    case Listenner.STATUS_SPEED_CHANGE: {
                                        LogUtil.i("SPEED == >" + getString(value));
                                    }break;
                                    case Listenner.STATUS_STOP: {
                                        LogUtil.i("STOP == >" + getString(value));
                                    }break;
                                    case Listenner.STATUS_SUCCESS: {
                                        LogUtil.i("SUCCESSFUL == >" + getString(value));
                                    }break;
                                }
                            }
                        })
                        .build();
                mDownloadThread.start();


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章