【Android架构】基于MVP模式的Retrofit2+RXjava封装之断点下载(五)

前言:最近有个断点下载的需求,捣鼓了下,然后分享下

关于文件下载,在第2篇中已经详细说过,这里就不在详细说了,先对之前的下载做个封装
首先是ApiServer

   @Streaming
   @GET
   /**
    * 大文件官方建议用 @Streaming 来进行注解,不然会出现IO异常,小文件可以忽略不注入
    */
   Observable<ResponseBody> downloadFile(@Url String fileUrl);

定义下载回调

public interface DownFileCallback {

    void onSuccess(String path);

    void onFail(String msg);

    void onProgress(long totalSize, long downSize);
}

DownLoadManager

public class DownLoadManager {

    private static DownLoadManager loadManager;

    private HashMap<String, FileObserver> hashMap;
    private OkHttpClient client;
    private Retrofit retrofit;
    private ApiServer apiServer;
    private DownFileCallback fileCallback;
   


    public DownLoadManager() {
        hashMap = new HashMap<>();
        client = new OkHttpClient.Builder()
                .addNetworkInterceptor(new Interceptor() {
                    @Override
                    public Response intercept(Chain chain) throws IOException {

                        Request request = chain.request();
 
                       Response response = chain.proceed(request);
                        return response.newBuilder().body(new ProgressResponseBody(response.body(),
                                new ProgressResponseBody.ProgressListener() {
                                    @Override
                                    public void onProgress(long totalSize, long downSize) {
                                        if (fileCallback != null) {
                                            fileCallback.onProgress(totalSize, downSize);
                                        }
                                    }
                                })).build();
                    }
                }).build();
        retrofit = new Retrofit.Builder().client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .baseUrl("https://wawa-api.vchangyi.com/").build();
        apiServer = retrofit.create(ApiServer.class);
    }

    public static DownLoadManager getInstance() {
        synchronized (Object.class) {
            if (loadManager == null) {
                loadManager = new DownLoadManager();
            }
        }
        return loadManager;
    }

 /**
     * 下载单个文件
     *
     * @param url
     * @param fileCallback
     */
    public void downFile(final String url, final DownFileCallback fileCallback) {
        //如果正在下载,则暂停
        if (isDownLoad(url)) {
            pause(url);
            return;
        }
        this.fileCallback = fileCallback;
        //存储的文件路径
        final String path = getTemporaryName(url);

        FileObserver observer = apiServer.downloadFile(url).map(new Function<ResponseBody, String>() {
            @Override
            public String apply(ResponseBody body) throws Exception {
                File file = FileUtil.saveFile(path, body);
                return file.getPath();
            }
        }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeWith(new FileObserver<String>() {
                    @Override
                    public void onSuccess(String path) {

                        fileCallback.onSuccess(path);
                        hashMap.remove(url);
                    }

                    @Override
                    public void onError(String msg) {
                        fileCallback.onFail(msg);
                        hashMap.remove(url);
                    }
                });
        //保存
        hashMap.put(url, observer);
    }
 /**
     * 暂停/取消任务
     *
     * @param url 完整url
     */
    public void pause(String url) {
        if (hashMap.containsKey(url)) {
            FileObserver observer = hashMap.get(url);
            if (observer != null) {
                observer.dispose();
                hashMap.remove(url);
            }
        }
    }

    /**
     * 获取临时文件名
     *
     * @param url
     * @return
     */
    public static String getTemporaryName(String url) {
        String type = "";
        if (url.contains(".")) {
            type = url.substring(url.lastIndexOf("."));
        }
        String dirName = Environment.getExternalStorageDirectory() + "/mvp/";

        File f = new File(dirName);
        //不存在创建
        if (!f.exists()) {
            f.mkdirs();
        }
        return dirName + System.currentTimeMillis() + type;
    }


    /**
     * 是否在下载
     *
     * @param url
     * @return
     */
    public boolean isDownLoad(String url) {
        return hashMap.containsKey(url);
    }


    public abstract class FileObserver<T> extends DisposableObserver<T> {

        @Override
        public void onNext(T t) {
            onSuccess(t);
        }

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

        @Override
        public void onComplete() {

        }


        public abstract void onSuccess(T o);

        public abstract void onError(String msg);
    }
}

断点下载的话,有2点要注意

  • 1.要添加RANGE请求头
if (downSize != 0 && totalSize != 0) {
                            request = request.newBuilder()
                                    .addHeader("RANGE", "bytes=" + downSize + "-" + totalSize).build();
                        }

downSizetotalSize,前者是下载的开始长度,后者是整个文件的长度,这些都需要我们暂停时,自己做保存。

  • 2.写文件问题,断点下载时,就不能从头开始写入文件,需要从上次结束的地方开始,这里就用到了RandomAccessFile
  /**
     * @param filePath
     * @param start    起始位置
     * @param body
     */
    public static File saveFile(String filePath, long start, ResponseBody body) {
        InputStream inputStream = null;
        RandomAccessFile raf = null;
        File file = null;
        try {
            file = new File(filePath);

            raf = new RandomAccessFile(filePath, "rw");
            inputStream = body.byteStream();
            byte[] fileReader = new byte[4096];
            //移动到该位置
            raf.seek(start);

            while (true) {
                int read = inputStream.read(fileReader);
                if (read == -1) {
                    break;
                }
                raf.write(fileReader, 0, read);
            }


        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (raf != null) {
                try {
                    raf.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return file;

    }

建个实体类,来保存文件相关属性,get和set就不贴了

public class DownModel {

    private String url;
    private String path;
    private String title;
    private String cover;
    private long totalSize;
    private long currentTotalSize;
    private long downSize;
    private boolean isExists;
    private boolean isFinish;
    private boolean isPause;
}
 /**
     * 下载单个文件
     *
     * @param downModel
     * @param
     */
    public void downFile(final DownModel downModel, final DownFileCallback fileCallback) {
        if (downModel == null) {
            return;
        }
        //如果正在下载,则暂停
        final String url = downModel.getUrl();
        if (isDownLoad(url)) {
            pause(url);
            Log.e("cheng", "pause url=" + url);
            return;
        }
        //当前链接
        currentUrl = url;
        //是否是断点下载
        if (downModel.getDownSize() != 0 && downModel.getTotalSize() != 0) {
            totalSize = downModel.getTotalSize();
            downSize = downModel.getDownSize();
            currentPath = downModel.getPath();
        } else {
            totalSize = 0;
            downSize = 0;
            currentPath = getTemporaryName(url);
            downModel.setPath(currentPath);
        }

        this.fileCallback = fileCallback;

        Log.e("cheng", "currentUrl=" + currentUrl);

        Log.e("cheng", "downSize=" + downSize + ",totalSize=" + totalSize + ",currentPath=" + currentPath);

        FileObserver observer = apiServer.downloadFile(url).map(new Function<ResponseBody, String>() {
            @Override
            public String apply(ResponseBody body) throws Exception {

                if (downModel.getDownSize() != 0 && downModel.getTotalSize() != 0) {
                    return FileUtil.saveFile(currentPath, downModel.getDownSize(), body).getPath();
                }
                File file = FileUtil.saveFile(currentPath, body);
                return file.getPath();
            }
        }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeWith(new FileObserver<String>() {
                    @Override
                    public void onSuccess(String path) {
                        downModel.setFinish(true);
                        downModel.setPath(path);
                        downModel.setExists(true);


                        if (fileCallback != null) {
                            fileCallback.onSuccess(path);
                        }
                        hashMap.remove(url);

                        currentUrl = null;
                    }

                    @Override
                    public void onError(String msg) {
                        if (fileCallback != null) {
                            fileCallback.onFail(msg);
                        }
                        hashMap.remove(url);
                        currentUrl = null;
                    }
                });
        //保存
        hashMap.put(url, observer);
    }

需要注意的是,如果文件总大小为50M,已下载的大小为10M,再次下载时onProgress返回的totalSize是文件总长度 减去 已下载大小 10M, 即40MdownSize为本次下载的已下载量

 private void down2() {
       String url = "http://download.sdk.mob.com/apkbus.apk";
       if (downModel == null) {
           downModel = new DownModel();
           downModel.setUrl(url);
       }
       DownLoadManager.getInstance().downFile(downModel, new DownFileCallback() {
           @Override
           public void onSuccess(String path) {
               showtoast("下载成功,path=" + path);
           }

           @Override
           public void onFail(String msg) {

           }

           @Override
           public void onProgress(long totalSize, long downSize) {
               Log.e("cheng", "totalSize =" + totalSize + ",downSize=" + downSize);
               if (downModel.getTotalSize() == 0) {
                   downModel.setTotalSize(totalSize);
               }
               downModel.setCurrentTotalSize(totalSize);

               downModel.setDownSize(downSize + downModel.getTotalSize() - downModel.getCurrentTotalSize());

               runOnUiThread(new Runnable() {
                   @Override
                   public void run() {
                       int progress = (int) (downModel.getDownSize() * 100 / downModel.getTotalSize());
                       tvDown.setText(progress + "%");
                       sbDown.setProgress(progress);
                   }
               });


           }
       });
   }

最后,献上源码 Github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容