15

I'm new to retrofit. I've searched but didn't found a simple answer. I want to know how can I show progress of download in Notification bar or at least show a progress dialog which specifies the percent of process and size of downloading file. Here is my code:

public interface ServerAPI {
    @GET
    Call<ResponseBody> downlload(@Url String fileUrl);

    Retrofit retrofit =
            new Retrofit.Builder()
                    .baseUrl("http://192.168.43.135/retro/") 
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();

}

public void download(){
    ServerAPI api = ServerAPI.retrofit.create(ServerAPI.class);
    api.downlload("https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_120x44dp.png").enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
            try {
                File path = Environment.getExternalStorageDirectory();
                File file = new File(path, "file_name.jpg");
                FileOutputStream fileOutputStream = new FileOutputStream(file);
                IOUtils.write(response.body().bytes(), fileOutputStream);
            }
            catch (Exception ex){
            }
        }


        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
        }
    });
}

please guide me if you can. thanks

Saman
  • 181
  • 1
  • 1
  • 7
  • it was a good solution to solve the problem:https://stackoverflow.com/questions/41892696/is-it-possible-to-show-progress-bar-when-download-via-retrofit-2-asynchronous/49398941#49398941 and I have test it,may it can help you – MichaelZ Mar 21 '18 at 05:46

4 Answers4

21

You need to create a specific OkHttp client which will intercept the network requests and send updates. This client should only be used for downloads.

First you are going to need an interface, like this one:

public interface OnAttachmentDownloadListener {
    void onAttachmentDownloadedSuccess();
    void onAttachmentDownloadedError();
    void onAttachmentDownloadedFinished();
    void onAttachmentDownloadUpdate(int percent);
}

Your download call should return a ResponseBody, which we will extend from to be able to get the download progress.

private static class ProgressResponseBody extends ResponseBody {

    private final ResponseBody responseBody;
    private final OnAttachmentDownloadListener progressListener;
    private BufferedSource bufferedSource;

    public ProgressResponseBody(ResponseBody responseBody, OnAttachmentDownloadListener progressListener) {
        this.responseBody = responseBody;
        this.progressListener = progressListener;
    }

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

                totalBytesRead += bytesRead != -1 ? bytesRead : 0;

                float percent = bytesRead == -1 ? 100f : (((float)totalBytesRead / (float) responseBody.contentLength()) * 100);

                if(progressListener != null)
                    progressListener.onAttachmentDownloadUpdate((int)percent);

                return bytesRead;
            }
        };
    }
}

Then you will need to create your OkHttpClient like this

public OkHttpClient.Builder getOkHttpDownloadClientBuilder(OnAttachmentDownloadListener progressListener) {
    OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();

    // You might want to increase the timeout
    httpClientBuilder.connectTimeout(20, TimeUnit.SECONDS);
    httpClientBuilder.writeTimeout(0, TimeUnit.SECONDS);
    httpClientBuilder.readTimeout(5, TimeUnit.MINUTES);

    httpClientBuilder.addInterceptor(new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            if(progressListener == null) return chain.proceed(chain.request());

        Response originalResponse = chain.proceed(chain.request());
        return originalResponse.newBuilder()
                .body(new ProgressResponseBody(originalResponse.body(), progressListener))
                .build();
        }
    });

    return httpClientBuilder;
}

Finally you only have to create your Retrofit client a different way, by passing your new OkHttp client. Based on your code, you can use something like this:

 public Retrofit getDownloadRetrofit(OnAttachmentDownloadListener listener) {

    return new Retrofit.Builder()
                .baseUrl("http://192.168.43.135/retro/") 
                .addConverterFactory(GsonConverterFactory.create())
                .client(getOkHttpDownloadClientBuilder(listener).build())
                .build();

}

Your listener will handle the creation of your notification or whatever else you want.

maxoumime
  • 3,191
  • 1
  • 16
  • 23
  • `onAttachmentDownloadUpdate()` recieved after `onResponse()`. it means file is downloaded. – Divy Soni Aug 26 '20 at 08:38
  • This approach may cause OutOfMemoryError for large files (for me it was 400+mb). I ended up using this answer: https://stackoverflow.com/a/65588935/7035703 – anro Jul 27 '21 at 11:36
8

Here is another Kotlin solution using Flow

interface MyService {
    @Streaming // allows streaming data directly to fs without holding all contents in ram
    @GET
    suspend fun getUrl(@Url url: String): ResponseBody
}

sealed class Download {
    data class Progress(val percent: Int) : Download()
    data class Finished(val file: File) : Download()
}

fun ResponseBody.downloadToFileWithProgress(directory: File, filename: String): Flow<Download> =
    flow {
        emit(Download.Progress(0))

        // flag to delete file if download errors or is cancelled
        var deleteFile = true
        val file = File(directory, "${filename}.${contentType()?.subtype}")

        try {
            byteStream().use { inputStream ->
                file.outputStream().use { outputStream ->
                    val totalBytes = contentLength()
                    val data = ByteArray(8_192)
                    var progressBytes = 0L

                    while (true) {
                        val bytes = inputStream.read(data)

                        if (bytes == -1) {
                            break
                        }

                        outputStream.channel
                        outputStream.write(data, 0, bytes)
                        progressBytes += bytes

                        emit(Download.Progress(percent = ((progressBytes * 100) / totalBytes).toInt()))
                    }

                    when {
                        progressBytes < totalBytes ->
                            throw Exception("missing bytes")
                        progressBytes > totalBytes ->
                            throw Exception("too many bytes")
                        else ->
                            deleteFile = false
                    }
                }
            }

            emit(Download.Finished(file))
        } finally {
            // check if download was successful

            if (deleteFile) {
                file.delete()
            }
        }
    }
        .flowOn(Dispatchers.IO)
        .distinctUntilChanged()

suspend fun Context.usage() {
    coroutineScope {
        myService.getUrl("https://www.google.com")
            .downloadToFileWithProgress(
                externalCacheDir!!,
                "my_file",
            )
            .collect { download ->
                when (download) {
                    is Download.Progress -> {
                        // update ui with progress
                    }
                    is Download.Finished -> {
                        // update ui with file
                    }
                }
            }
    }
}
Robert C.
  • 81
  • 1
  • 4
4

Here is my variant with Kotlin's coroutines

  1. Specify API interface. We need @Streaming annotation to say Retrofit that we want to handle the response body manually. Otherwise, retrofit will try to write your file straight into RAM
interface Api {

    @Streaming
    @GET("get-zip-ulr/{id}")
    fun getZip(@Path("id") id: Int): Call<ResponseBody>
}
  1. Create DataSource which will control downloading process
class FilesDataSource(private val parentFolder: File, private val api: Api) {

    suspend fun downloadZip(id: Int, processCallback: (Long, Long) -> Unit): File {
        val response = api.getZip(id).awaitResponse()// returns the response, but it's content will be later
        val body = response.body()
        if (response.isSuccessful && body != null) {
            val file = File(parentFolder, "$id")
            body.byteStream().use { inputStream ->
                FileOutputStream(file).use { outputStream ->
                    val data = ByteArray(8192)
                    var read: Int
                    var progress = 0L
                    val fileSize = body.contentLength()
                    while (inputStream.read(data).also { read = it } != -1) {
                        outputStream.write(data, 0, read)
                        progress += read
                        publishProgress(processCallback, progress, fileSize)
                    }
                    publishProgress(processCallback, fileSize, fileSize)
                }
            }
            return file
        } else {
            throw HttpException(response)
        }
    }

    private suspend fun publishProgress(
        callback: (Long, Long) -> Unit,
        progress: Long, //bytes
        fileSize: Long  //bytes
    ) {
        withContext(Dispatchers.Main) { // invoke callback in UI thtread
            callback(progress, fileSize)
        }
    }
}

Now you can execute downloadZip() method in your ViewModel or Presenter and give it a callback which will be linked to some ProgerssBar. After download completion, you will receive the downloaded file.

  • 1
    doesn't it load the data first & then give the input stream? because when I have added the `HttpLoggingInterceptor` to retrofit, I see all bytes are downloaded first via stream and the progress is not actual download progress but copy from the input stream to another. Let me know if I am incorrect. – NAUSHAD Aug 06 '20 at 20:44
-3

you can take a look here, you dont have to implement it by yourself ,the idea behind is to take the content-length of the request and when you write on the buffer just calculate your progress

Community
  • 1
  • 1
Lior
  • 829
  • 6
  • 6
  • The link you provided refers to uploading files not downloading, so the implementation is different. – w3bshark Aug 31 '18 at 19:51