Downloading files is a relatively common requirement. Given a url, we can use URLConnection to download the file . Files can also be downloaded via the stream using Android OkHttp. Add an interceptor to OkHttp to implement the monitoring of the download progress.

Add an interceptor to OkHttp to implement the monitoring of the download progress.

Use streams to download files

The code can refer to: android-Basic4/tree/master/appdowloadsample

To get and use a byte stream, you need to pay attention to two main points, one is the @Streaming annotation of the service interface method, and the other is to get the ResponseBody.

Get the stream (Stream). First define a service ApiService. Add a @Streaming annotation to the method.

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

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();

Initiate a network request. Get the 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 throwable) throws 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() {

            }
        });

Get the byte stream body.byteStream() via ResponseBody. Here we will first create a temporary file tmpFile to write the data to a temporary file.
After the download is complete, rename it to the target file 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);
        }
    }
}

Each time the data is read, it counts how much data was read and how much time was spent. After the speed limit is exceeded, take the initiative to sleep to achieve the effect of controlling the download speed.

Be careful not to sleep for too long, so that the socket is not closed. This control is the speed of reading and writing network data streams and local files.

Download progress monitor

Android OkHttp implements download progress monitoring, which can be started from the reading and writing of byte stream. You can also use an interceptor, refer to the official example .
Here, the interception method is used to implement the network download progress monitoring function.

Define callbacks and network interceptors

Define the callback first.

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

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;
            }
        };
    }
}

Define an interceptor. 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();
    }
}

Add interceptor

Add 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();

It is worth noting that the progress here is very frequent. It is not necessary to update the UI every time the callback is made.