Downloading files is a common requirement. Given a URL, we can download the file using URLConnection. Files can also be downloaded via streams using OkHttp. Add an interceptor to OkHttp to monitor the download progress.

Use streams to download files

To get and use the byte stream, note the @Streaming annotation of the service interface method and the ResponseBody annotation.

Get the Stream. Start by defining a service, ApiService. Annotate the method with @streaming.

private interface ApiService {
        @Streaming
        @GET
        Observable<ResponseBody> download(@Url String url);
    }
Copy the code

Initialize OkHttp. Remember to fill in your baseUrl.

OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .connectTimeout(8, TimeUnit.SECONDS)
            .build();

    retrofit = new Retrofit.Builder()
            .client(okHttpClient)
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .baseUrl("https://yourbaseurl.com")
            .build();
Copy the code

Initiate a network request. Get ResponseBody.

String downUrl = "xxx.com/aaa.apk"; retrofit.create(ApiService.class) .download(downUrl) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .doOnNext(new Consumer<ResponseBody>() { @Override public void accept(ResponseBody responseBody) throws Exception { // DoOnError (new Consumer<Throwable>() {@override public void accept(Throwable) throws ResponseBody  Exception { Log.e(TAG, "accept on error: " + downUrl, throwable); } }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer<ResponseBody>() { @Override public void onSubscribe(Disposable d) { } @Override public void onNext(ResponseBody responseBody) { } @Override public void onError(Throwable e) { Log.e(TAG, "Download center retrofit onError: ", e); } @Override public void onComplete() { } });Copy the code

Get the body.bytestream () stream via ResponseBody. A temporary file called tmpFile is created to write data to the temporary file. After downloading, rename the file to targetFile.

public void saveFile(ResponseBody body) { state = DownloadTaskState.DOWNLOADING; byte[] buf = new byte[2048]; int len; FileOutputStream fos = null; try { Log.d(TAG, "saveFile: body content length: " + body.contentLength()); srcInputStream = body.byteStream(); File dir = tmpFile.getParentFile(); if (dir == null) { throw new FileNotFoundException("target file has no dir."); } if (! dir.exists()) { boolean m = dir.mkdirs(); onInfo("Create dir " + m + ", " + dir); } File file = tmpFile; if (! file.exists()) { boolean c = file.createNewFile(); onInfo("Create new file " + c); } fos = new FileOutputStream(file); long time = System.currentTimeMillis(); while ((len = srcInputStream.read(buf)) ! = 1 &&! isCancel) { fos.write(buf, 0, len); int duration = (int) (System.currentTimeMillis() - time); int overBytes = len - downloadBytePerMs() * duration; if (overBytes > 0) { try { Thread.sleep(overBytes / downloadBytePerMs()); } catch (Exception e) { e.printStackTrace(); } } time = System.currentTimeMillis(); if (isCancel) { state = DownloadTaskState.CLOSING; srcInputStream.close(); break; } } if (! isCancel) { fos.flush(); boolean rename = tmpFile.renameTo(targetFile); if (rename) { setState(DownloadTaskState.DONE); onSuccess(url); } else { setState(DownloadTaskState.ERROR); onError(url, new Exception("Rename file fail. " + tmpFile)); } } } catch (FileNotFoundException e) { Log.e(TAG, "saveFile: FileNotFoundException ", e); setState(DownloadTaskState.ERROR); onError(url, e); } catch (Exception e) { Log.e(TAG, "saveFile: IOException ", e); setState(DownloadTaskState.ERROR); onError(url, e); } finally { try { if (srcInputStream ! = null) { srcInputStream.close(); } if (fos ! = null) { fos.close(); } } catch (IOException e) { Log.e(TAG, "saveFile", e); } if (isCancel) { onCancel(url); }}}Copy the code

Each read cycle counts how much data is read and how long it takes. Take the initiative to sleep after exceeding the speed limit to achieve the effect of controlling the download speed. Be careful not to sleep too long, lest the socket be closed. This controls network data flow and local file read and write speed.

Download progress monitoring

OkHttp implements download progress monitoring, which can start from byte stream reading and writing. Interceptors can also be used. See the official example. The interceptor is used to monitor the progress of network download.

Define callbacks and network interceptors

Define the callback first.

public interface ProgressListener {
    void update(String url, long bytesRead, long contentLength, boolean done);
}
Copy the code

Customize the ProgressResponseBody.

public class ProgressResponseBody extends ResponseBody { private final ResponseBody responseBody; private final ProgressListener progressListener; private BufferedSource bufferedSource; private final String url; ProgressResponseBody(String url, ResponseBody responseBody, ProgressListener progressListener) { this.responseBody = responseBody; this.progressListener = progressListener; this.url = url; } @Override public MediaType contentType() { return responseBody.contentType(); } @Override public long contentLength() { return responseBody.contentLength(); } @Override public BufferedSource source() { if (bufferedSource == null) { bufferedSource = Okio.buffer(source(responseBody.source())); } return bufferedSource; } private Source source(final Source source) { return new ForwardingSource(source) { long totalBytesRead = 0L; @Override public long read(Buffer sink, long byteCount) throws IOException { long bytesRead = super.read(sink, byteCount); // read() returns the number of bytes read, or -1 if this source is exhausted. totalBytesRead += bytesRead ! = 1? bytesRead : 0; progressListener.update(url, totalBytesRead, responseBody.contentLength(), bytesRead == -1); return bytesRead; }}; }}Copy the code

Define interceptors. Get information from Response.

public class ProgressInterceptor implements Interceptor { private ProgressListener progressListener; public ProgressInterceptor(ProgressListener progressListener) { this.progressListener = progressListener; } @NotNull @Override public Response intercept(@NotNull Chain chain) throws IOException { Response originalResponse = chain.proceed(chain.request()); return originalResponse.newBuilder() .body(new ProgressResponseBody(chain.request().url().url().toString(), originalResponse.body(), progressListener)) .build(); }}Copy the code
Adding interceptors

Add the ProgressInterceptor when creating OkHttpClient.

OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .connectTimeout(8, TimeUnit.SECONDS)
            .addInterceptor(new ProgressInterceptor(new ProgressListener() {
                @Override
                public void update(String url, long bytesRead, long contentLength, boolean done) {
                    // tellProgress(url, bytesRead, contentLength, done);
                }
            }))
            .build();
Copy the code

It is worth noting that progress updates are quite frequent here. You don’t have to update the UI every time you call back.

Android development: framework source code parsing video reference