96

I'am currently using Retrofit 2 and i want to upload some photo at my server. I know, that older version uses TypedFile class for uploading. And if we want to use progress bar with it we should override writeTo method in TypedFile class.

Is it possible to show progress when using retrofit 2 library?

Sagar Zala
  • 4,854
  • 9
  • 34
  • 62
Yuriy Kolbasinskiy
  • 3,791
  • 3
  • 16
  • 33
  • For those still stuck on Retrofit 1.x, here's an easy, working solution that we ended up using: https://stackoverflow.com/a/24772058/293280 – Joshua Pinter Jul 05 '18 at 20:42

17 Answers17

186

First of all, you should use Retrofit 2 version equal to or above 2.0 beta2. Second, create new class extends RequestBody:

public class ProgressRequestBody extends RequestBody {
    private File mFile;
    private String mPath;
    private UploadCallbacks mListener;
    private String content_type;

  private static final int DEFAULT_BUFFER_SIZE = 2048;

    public interface UploadCallbacks {
        void onProgressUpdate(int percentage);
        void onError();
        void onFinish();
    }

Take note, I added content type so it can accommodate other types aside image

public ProgressRequestBody(final File file, String content_type,  final  UploadCallbacks listener) {
    this.content_type = content_type;
    mFile = file;
    mListener = listener;            
}



@Override
    public MediaType contentType() {
        return MediaType.parse(content_type+"/*");
    }

@Override
public long contentLength() throws IOException {
  return mFile.length();
}

@Override
public void writeTo(BufferedSink sink) throws IOException {
    long fileLength = mFile.length();
    byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
    FileInputStream in = new FileInputStream(mFile);
    long uploaded = 0;

try {
            int read;
            Handler handler = new Handler(Looper.getMainLooper());
            while ((read = in.read(buffer)) != -1) {

            // update progress on UI thread
                handler.post(new ProgressUpdater(uploaded, fileLength));

                uploaded += read;
                sink.write(buffer, 0, read);
            }
        } finally {
            in.close();
        }
}

private class ProgressUpdater implements Runnable {
        private long mUploaded;
        private long mTotal;
        public ProgressUpdater(long uploaded, long total) {
            mUploaded = uploaded;
            mTotal = total;
        }

        @Override
        public void run() {
            mListener.onProgressUpdate((int)(100 * mUploaded / mTotal));            
        }
    }
}

Third, create the interface

@Multipart
    @POST("/upload")        
    Call<JsonObject> uploadImage(@Part MultipartBody.Part file);

/* JsonObject above can be replace with you own model, just want to make this notable. */

Now you can get progress of your upload. In your activity (or fragment):

class MyActivity extends AppCompatActivity implements ProgressRequestBody.UploadCallbacks {

        ProgressBar progressBar;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            progressBar = findViewById(R.id.progressBar);

            ProgressRequestBody fileBody = new ProgressRequestBody(file, this);
            MultipartBody.Part filePart = 

            MultipartBody.Part.createFormData("image", file.getName(), fileBody);

            Call<JsonObject> request = RetrofitClient.uploadImage(filepart);

             request.enqueue(new Callback<JsonObject>() {
             @Override
             public void onResponse(Call<JsonObject> call,   Response<JsonObject> response) {
                if(response.isSuccessful()){
                /* Here we can equally assume the file has been downloaded successfully because for some reasons the onFinish method might not be called, I have tested it myself and it really not consistent, but the onProgressUpdate is efficient and we can use that to update our progress on the UIThread, and we can then set our progress to 100% right here because the file already downloaded finish. */
                  }
            }

            @Override
            public void onFailure(Call<JsonObject> call, Throwable t) {
                      /* we can also stop our progress update here, although I have not check if the onError is being called when the file could not be downloaded, so I will just use this as a backup plan just in case the onError did not get called. So I can stop the progress right here. */
            }
        });

      }

        @Override
        public void onProgressUpdate(int percentage) {
            // set current progress
            progressBar.setProgress(percentage);
        }

        @Override
        public void onError() {
            // do something on error
        }

        @Override
        public void onFinish() {
            // do something on upload finished,
            // for example, start next uploading at a queue
            progressBar.setProgress(100);
        }

}
Daniel
  • 2,415
  • 3
  • 24
  • 34
Yuriy Kolbasinskiy
  • 3,791
  • 3
  • 16
  • 33
  • 18
    I am seeing the data written to the sink nearly immediately, however the actual call is taking several seconds on a slow connection. I've tried adding sink.flush() to the call after write, but it still appears to show the timing of an internal transfer of the data rather than the progress of transmitting over the network. – Jake Hall Nov 21 '15 at 00:15
  • Can you give an example, how to show progress bar on download? – Anton Shkurenko Nov 22 '15 at 20:36
  • @AntonShkurenko what you mean on "progress bar" ? Simply progress bar from standart library or you want to use progress with percentage or something else? – Yuriy Kolbasinskiy Nov 23 '15 at 07:20
  • I'm about progress callback, when downloading file with Retrofit. – Anton Shkurenko Nov 23 '15 at 11:05
  • @AntonShkurenko hmm, i don't understand what is your problem. Just implement UploadCallback on your activity/fragment. I've added some example for this. – Yuriy Kolbasinskiy Nov 25 '15 at 04:03
  • You posted example for request body, I'm about response, it this the same? – Anton Shkurenko Nov 25 '15 at 19:10
  • @YuriyKolbasinskiy with your comment `code` // without filename field it doesn't work :( // upload is a POST field Call uploadImage(@Part("upload\"; filename=\"1\" ") ProgressRequestBody file);`code` you saved from almost a week looking for why uploading a file was NOT working, with Charles proxy running out of memory...it was a mess! Thanks again – gropapa Feb 10 '16 at 23:55
  • @AntonShkurenko the answer is about RequestBody with is the parameter for an attached file, it is also responsible for writing itself on the outputStream, so the answer shows how to notify the progress of the RequestBody currently being written, when you receive a Response, it is already all over so...100% is done – gropapa Feb 11 '16 at 00:01
  • @YuriyKolbasinskiy Is there a way to use it in conjunction with RxJava & Observable? – Sree Mar 16 '16 at 19:52
  • @Sree i haven't good skills with RxJava. Answer for your question is dependent on what are you wanted. Can you give me an example of your code? – Yuriy Kolbasinskiy Mar 17 '16 at 06:46
  • Actually, never mind. I think I don't need to worry about RxJava and Observables – Sree Mar 17 '16 at 17:22
  • 2
    @JakeHall I am seeing the same, did you find any solution for that? – Sandip Fichadiya Mar 20 '16 at 11:26
  • @sam_0829 no, I just moved to an indeterminate progress bar :( – Jake Hall Mar 28 '16 at 16:25
  • 7
    handler.post(new ProgressUpdater(uploaded, fileLength)); This line should at the bottom of while loop, otherwise you'll always receive 99%. – codezjx Apr 18 '16 at 11:20
  • it gives typecasting error when i call rest api. eg restclient.uploadImage( file); – CodingRat Apr 21 '16 at 07:06
  • @CodingRat please give me some of your code and stacktrace – Yuriy Kolbasinskiy Apr 23 '16 at 01:59
  • 11
    I'm also facing the same issue with JakeHall and couldn't find any solution... writeTo method gets called twice and first time it loads immediately but second time is the real uploading period that we seek. Do you have any comment on that @YuriyKolbasinskiy ? – yahya May 10 '16 at 10:43
  • 28
    @yahya I was seeing the same thing and found that it was caused by the HttpLoggingInterceptor I'd set up. The first, quick load is HttpLoggingInterceptor calling writeTo and passing in a local buffer just for logging purposes. – Devin Pitcher Jun 04 '16 at 03:21
  • 1
    @pinch wow! I never thought so. I had HttpLoggingInterceptor included as well. Thanks for clarification :) – yahya Jun 06 '16 at 05:38
  • I would recommend usage of Otto library with some class ProgressEvent. That way you can register to bus wherever you'd like to (service, activity, fragment) and you dont have to worry if activity is alive or not when callback fires. – bajicdusko Jun 09 '16 at 11:39
  • @bajicdusko thanks for your comment. Yes, you can use some date bus for update UI progress. But if you want to use it you should subscribe and unsubscribe on all onResume and onPause method of your `Activity`/`Fragment` and if your file was uploaded/failed when you UI is on pause, you should check the upload status. That is if your using date bus, you should cache your result somewhere. – Yuriy Kolbasinskiy Jun 10 '16 at 02:22
  • @YuriyKolbasinskiy I've used your example from above and modified it to be used with bus. Bus is registered and unregistered in Base class for activity, fragment or service. My case is a little bit different, because i am executing upload from background service via JobManager, and ProgressRequestBody is posting events to bus which is registered in service, even if application is dead it is working. Service also updates progress inside notification, which is really sufficient. If you start your app in the meantime, your activity will register to bus and it will receive current progress. – bajicdusko Jun 10 '16 at 09:08
  • @YuriyKolbasinskiy in your Answer you have to implement one other method in ProgressRequestBody.java: `@Override public long contentLength() throws IOException { return mFile.length(); }` I just find this solution – Harsh Dalwadi Jun 28 '16 at 06:50
  • This doesn't take in multipart form parameters with it :( – Bhargav Aug 29 '16 at 09:35
  • @Yuriy Kolbasinskiy Can you tell how to convert RequestBody to ProgressRequestBody in my activity? – Hardik Vaghasiya Sep 16 '16 at 08:53
  • @YuriyKolbasinskiy I am using progress dialog. In progress dialog Progress is not Updating. I Used `setProgressDialog()` instead of `setProgress()` rest of code is as is it as above your posted code. First I was sending multiple files but then i tried on single file but it isn't working for me – Saurabh Bhandari Jan 10 '17 at 10:29
  • @SaurabhBhandari `ProgressDialog` doesn't have `setProgressDialog()` method, post your code, i can't understand what are you doing at your project – Yuriy Kolbasinskiy Jan 11 '17 at 01:58
  • @YuriyKolbasinskiy the UI thread seems to be blocked at regular intervals when a use this approach :/ – Sam Berg Feb 18 '17 at 08:43
  • @SamBerg You are right, doing Handler handler = new Handler(Looper.getMainLooper()); and posting on ui thread makes this unusable. Remove handler and handler.post(new ProgressUpdater(uploaded, fileLength)); instead call listener directly mListener.onProgressUpdate((int)(100 * uploaded / fileLength)); That has unblocked my UI and works fine now – voytez Feb 24 '17 at 18:19
  • Uploaded file was corrupted using this way and actually most of the solutions had the same problem but finally i found a correct way! I've posted it as an answer here.Easy ! http://stackoverflow.com/a/43310377/4799599 – Siamak Apr 09 '17 at 18:37
  • 2
    How can I upload multiple files using this? – Hardik Mamtora Apr 13 '17 at 13:54
  • @HardikMamtora you can use thread (thread pool) and queue of files for do you task. – Yuriy Kolbasinskiy Apr 13 '17 at 15:23
  • @YuriyKolbasinskiy how can we pass other parameters in parts(body) and headers in this type of request. – nadafafif May 02 '17 at 11:47
  • i want to achieve this for addNewAttachment(@Header(USER_ID) String userId, @Header(AUTHORIZATION) String authCode, @Part(CARD_ID) RequestBody cardId, @Part(ATTACHEMNT_NAME) RequestBody fileNamePart, @Part(ATTACHED_FILE) MultipartBody.Part filePart, @Part(DOCUMENT_SIZE) RequestBody fileSizePart); request – nadafafif May 02 '17 at 12:03
  • Use this get the progressBar until 100 (moved `handler.post(new ProgressUpdater(uploaded, fileLength));` to end of while loop suggested by @codezjx `try { int read; Handler handler = new Handler(Looper.getMainLooper()); while ((read = in.read(buffer)) != -1) { uploaded += read; sink.write(buffer, 0, read); // update progress on UI thread handler.post(new ProgressUpdater(uploaded, fileLength)); } } finally { in.close(); }` – Minion May 11 '17 at 07:49
  • 1
    I didn't find use of void onError(); void onFinish(); – Lalit Jadav Sep 01 '17 at 10:46
  • it't working fine but how can get the finish call after image uploded successfully it's stopped progress on 99/100 – Mahendra Dabi Sep 14 '17 at 06:41
  • 4
    How can i show progress for MultipartBody.Part[] , while uploading multiple files in single request? – Usman Rana Dec 15 '17 at 11:22
  • 1
    This does not work while sending text data and file both together, showing the progress twice – Bhavik Mehta Feb 01 '18 at 08:51
  • The loader is working fine but i have an issue, if i attach one file to the request body the progress bar is showing twice. on first pop up it will show progress 1 to 100 very fastly on second time it shows the actual progress of uploading. – Devidas M Das May 17 '18 at 06:22
  • @DevidasMDas did u find any solution please update here. – Raj Kumar Sep 27 '18 at 06:47
  • 1
    get File upload value like percentage into two times. –  Oct 23 '18 at 05:37
  • 1
    Comment out HttpLoggingInterceptor to prevent writeTo method gets called twice – Afjalur Rahman Rana Jun 10 '19 at 03:12
  • As a new person, it took me one hour to find out that this "ProgressRequestBody fileBody = new ProgressRequestBody(file, this);" should have been like this "ProgressRequestBody fileBody = new ProgressRequestBody(file, content_type, this);" – Abdullah Apr 09 '20 at 11:58
  • 1
    Did any one found a fix for the @JakeHall issue? I am also running into the same issue. Removing HttpLoggingInterceptor did not help either. – Vikalp Jul 22 '20 at 12:23
  • what does DEFAULT_BUFFER_SIZE mean ? is it the maximum allowable file size ? – Alexa289 Nov 04 '20 at 05:03
  • I am getting progress even after canceling request `call.cancel()`. any body please tell me how to cancel progress along with request? – Abdul Salam Mar 02 '21 at 16:49
28

Modified Yuriy Kolbasinskiy's to use rxjava and use kotlin. Added a workaround for using HttpLoggingInterceptor at the same time

class ProgressRequestBody : RequestBody {

    val mFile: File
    val ignoreFirstNumberOfWriteToCalls : Int


    constructor(mFile: File) : super(){
        this.mFile = mFile
        ignoreFirstNumberOfWriteToCalls = 0
    }

    constructor(mFile: File, ignoreFirstNumberOfWriteToCalls : Int) : super(){
        this.mFile = mFile
        this.ignoreFirstNumberOfWriteToCalls = ignoreFirstNumberOfWriteToCalls
    }


    var numWriteToCalls = 0

    protected val getProgressSubject: PublishSubject<Float> = PublishSubject.create<Float>()

    fun getProgressSubject(): Observable<Float> {
        return getProgressSubject
    }


    override fun contentType(): MediaType {
        return MediaType.parse("video/mp4")
    }

    @Throws(IOException::class)
    override fun contentLength(): Long {
        return mFile.length()
    }

    @Throws(IOException::class)
    override fun writeTo(sink: BufferedSink) {
        numWriteToCalls++

        val fileLength = mFile.length()
        val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
        val `in` = FileInputStream(mFile)
        var uploaded: Long = 0

        try {
            var read: Int
            var lastProgressPercentUpdate = 0.0f
            read = `in`.read(buffer)
            while (read != -1) {

                uploaded += read.toLong()
                sink.write(buffer, 0, read)
                read = `in`.read(buffer)

                // when using HttpLoggingInterceptor it calls writeTo and passes data into a local buffer just for logging purposes.
                // the second call to write to is the progress we actually want to track
                if (numWriteToCalls > ignoreFirstNumberOfWriteToCalls ) {
                    val progress = (uploaded.toFloat() / fileLength.toFloat()) * 100f
                    //prevent publishing too many updates, which slows upload, by checking if the upload has progressed by at least 1 percent
                    if (progress - lastProgressPercentUpdate > 1 || progress == 100f) {
                        // publish progress
                        getProgressSubject.onNext(progress)
                        lastProgressPercentUpdate = progress
                    }
                }
            }
        } finally {
            `in`.close()
        }
    }


    companion object {

        private val DEFAULT_BUFFER_SIZE = 2048
    }
}

An example video upload interface

public interface Api {

    @Multipart
    @POST("/upload")        
    Observable<ResponseBody> uploadVideo(@Body MultipartBody requestBody);
}

An example function to post a video:

fun postVideo(){
            val api : Api = Retrofit.Builder()
            .client(OkHttpClient.Builder()
                    //.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
                    .build())
            .baseUrl("BASE_URL")
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build()
            .create(Api::class.java)

    val videoPart = ProgressRequestBody(File(VIDEO_URI))
    //val videoPart = ProgressRequestBody(File(VIDEO_URI), 1) //HttpLoggingInterceptor workaround
    val requestBody = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("example[name]", place.providerId)
            .addFormDataPart("example[video]","video.mp4", videoPart)
            .build()

    videoPart.getProgressSubject()
            .subscribeOn(Schedulers.io())
            .subscribe { percentage ->
                Log.i("PROGRESS", "${percentage}%")
            }

    var postSub : Disposable?= null
    postSub = api.postVideo(requestBody)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({ r ->
            },{e->
                e.printStackTrace()
                postSub?.dispose();

            }, {
                Toast.makeText(this,"Upload SUCCESS!!",Toast.LENGTH_LONG).show()
                postSub?.dispose();
            })
}
Sagar Zala
  • 4,854
  • 9
  • 34
  • 62
luca992
  • 1,548
  • 22
  • 27
  • 3
    This is just showing the write speed not upload speed, This in progress seems like we have updated progress in for loop from 0 to 100, as my upload is not as fast as progress shows – Akash Dubey Apr 10 '19 at 12:18
  • 1
    @AkashDubey sounds like you are using the HttpLoggingInterceptor, but did not use the commented out work-around I provided – luca992 Apr 11 '19 at 16:20
  • sorry my bad, my file was too small , hence gets uploaded faster, Its working fine – Akash Dubey Apr 12 '19 at 03:54
  • 1
    I am also facing the same issue what @AkashDubey pointed out. Using the workaround for HttpLoggingInterceptor did not help. It just prevents the writeTo logic getting executed twice and data is still written to the sink immediately in the next writeTo call. – Vikalp Jul 22 '20 at 12:27
  • what does DEFAULT_BUFFER_SIZE mean ? is it the maximum allowable file size ? – Alexa289 Nov 04 '20 at 05:01
12

Here's how to handle upload file progress with a simple POST rather than Multipart. For multipart check out @Yariy's solution. Additionally, this solution uses Content URI's instead of direct file references.

RestClient

@Headers({
    "Accept: application/json",
    "Content-Type: application/octet-stream"
})
@POST("api/v1/upload")
Call<FileDTO> uploadFile(@Body RequestBody file);

ProgressRequestBody

public class ProgressRequestBody extends RequestBody {
    private static final String LOG_TAG = ProgressRequestBody.class.getSimpleName();

    public interface ProgressCallback {
        public void onProgress(long progress, long total);
    }

    public static class UploadInfo {
        //Content uri for the file
        public Uri contentUri;

        // File size in bytes
        public long contentLength;
    }

    private WeakReference<Context> mContextRef;
    private UploadInfo mUploadInfo;
    private ProgressCallback mListener;

    private static final int UPLOAD_PROGRESS_BUFFER_SIZE = 8192;

    public ProgressRequestBody(Context context, UploadInfo uploadInfo, ProgressCallback listener) {
        mContextRef = new WeakReference<>(context);
        mUploadInfo =  uploadInfo;
        mListener = listener;
    }

    @Override
    public MediaType contentType() {
        // NOTE: We are posting the upload as binary data so we don't need the true mimeType
        return MediaType.parse("application/octet-stream");
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        long fileLength = mUploadInfo.contentLength;
        byte[] buffer = new byte[UPLOAD_PROGRESS_BUFFER_SIZE];
        InputStream in = in();
        long uploaded = 0;

        try {
            int read;
            while ((read = in.read(buffer)) != -1) {
                mListener.onProgress(uploaded, fileLength);

                uploaded += read;

                sink.write(buffer, 0, read);
            }
        } finally {
            in.close();
        }
    }

    /**
     * WARNING: You must override this function and return the file size or you will get errors
     */
    @Override
    public long contentLength() throws IOException {
        return mUploadInfo.contentLength;
    }

    private InputStream in() throws IOException {
        InputStream stream = null;
        try {
            stream = getContentResolver().openInputStream(mUploadInfo.contentUri);            
        } catch (Exception ex) {
            Log.e(LOG_TAG, "Error getting input stream for upload", ex);
        }

        return stream;
    }

    private ContentResolver getContentResolver() {
        if (mContextRef.get() != null) {
            return mContextRef.get().getContentResolver();
        }
        return null;
    }
}

To initiate the upload:

// Create a ProgressRequestBody for the file
ProgressRequestBody requestBody = new ProgressRequestBody(
    getContext(),
    new UploadInfo(myUri, fileSize),
    new ProgressRequestBody.ProgressCallback() {
        public void onProgress(long progress, long total) {
            //Update your progress UI here
            //You'll probably want to use a handler to run on UI thread
        }
    }
);

// Upload
mRestClient.uploadFile(requestBody);

Warning, if you forget to override the contentLength() function you may receive a few obscure errors:

retrofit2.adapter.rxjava.HttpException: HTTP 503 client read error

Or

Write error: ssl=0xb7e83110: I/O error during system call, Broken pipe

Or

javax.net.ssl.SSLException: Read error: ssl=0x9524b800: I/O error during system call, Connection reset by peer

These are a result of RequestBody.writeTo() being called multiple times as the default contentLength() is -1.

Anyways this took a long time to figure out, hope it helps.

Useful links: https://github.com/square/retrofit/issues/1217

Justin Fiedler
  • 6,478
  • 3
  • 21
  • 25
  • 1
    **RequestBody.writeTo() being called multiple times**. Did you solved this one? Cause I am facing this one. It's calling twice! – android_griezmann Nov 24 '16 at 13:12
  • @android_griezmann Check that you are setting the correct contentLength. There is an issue with the file sizes returned via Cursor and OpenableColumns.FILE_SIZE, try using ParcelFileDescriptor.getStatSize() instead as described here: http://stackoverflow.com/questions/21882322/how-to-correctly-get-the-file-size-of-an-android-net-uri – Justin Fiedler Nov 28 '16 at 22:01
  • it is becouse you use interceptor – zihadrizkyef Jan 22 '18 at 04:20
5

@luca992 Thank you for your answer. I have implemented this in JAVA and now it is working fine.

public class ProgressRequestBodyObservable extends RequestBody {

    File file;
    int ignoreFirstNumberOfWriteToCalls;
    int numWriteToCalls;`enter code here`

    public ProgressRequestBodyObservable(File file) {
        this.file = file;

        ignoreFirstNumberOfWriteToCalls =0;
    }

    public ProgressRequestBodyObservable(File file, int ignoreFirstNumberOfWriteToCalls) {
        this.file = file;
        this.ignoreFirstNumberOfWriteToCalls = ignoreFirstNumberOfWriteToCalls;
    }


    PublishSubject<Float> floatPublishSubject = PublishSubject.create();

   public Observable<Float> getProgressSubject(){
        return floatPublishSubject;
    }

    @Override
    public MediaType contentType() {
        return MediaType.parse("image/*");
    }

    @Override
    public long contentLength() throws IOException {
        return file.length();
    }



    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        numWriteToCalls++;


        float fileLength = file.length();
        byte[] buffer = new byte[2048];
        FileInputStream in = new  FileInputStream(file);
        float uploaded = 0;

        try {
            int read;
            read = in.read(buffer);
            float lastProgressPercentUpdate = 0;
            while (read != -1) {

                uploaded += read;
                sink.write(buffer, 0, read);
                read = in.read(buffer);

                // when using HttpLoggingInterceptor it calls writeTo and passes data into a local buffer just for logging purposes.
                // the second call to write to is the progress we actually want to track
                if (numWriteToCalls > ignoreFirstNumberOfWriteToCalls ) {
                    float progress = (uploaded / fileLength) * 100;
                    //prevent publishing too many updates, which slows upload, by checking if the upload has progressed by at least 1 percent
                    if (progress - lastProgressPercentUpdate > 1 || progress == 100f) {
                        // publish progress
                        floatPublishSubject.onNext(progress);
                        lastProgressPercentUpdate = progress;
                    }
                }
            }
        } finally {
        in.close();
        }

    }
}
labhya sharma
  • 47
  • 1
  • 2
5

Remove the Http Logging interceptor from httpbuilder. Else it will call writeTo() twice. Or change logging Level from BODY.

Dmitriy
  • 5,525
  • 12
  • 25
  • 38
2

I update progressbar onProgressUpdate. This code can get better performance.

@Override
public void writeTo(BufferedSink sink) throws IOException {
    long fileLength = mFile.length();
    byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
    FileInputStream in = new FileInputStream(mFile);
    long uploaded = 0;

    try {
        int read;
        Handler handler = new Handler(Looper.getMainLooper());
        int num = 0;
        while ((read = in.read(buffer)) != -1) {

            int progress = (int) (100 * uploaded / fileLength);
            if( progress > num + 1 ){
                // update progress on UI thread
                handler.post(new ProgressUpdater(uploaded, fileLength));
                num = progress;
            }

            uploaded += read;
            sink.write(buffer, 0, read);
        }
    } finally {
        in.close();
    }
}
2

This is the Extension function for Kotlin.

/** Returns a new request body that transmits the content of this. */
fun File.asRequestBodyWithProgress(
    contentType: MediaType? = null,
    progressCallback: ((progress: Float) -> Unit)?
): RequestBody {
    return object : RequestBody() {
        override fun contentType() = contentType

        override fun contentLength() = length()

        override fun writeTo(sink: BufferedSink) {
            val fileLength = contentLength()
            val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
            val inSt = FileInputStream(this@asRequestBodyWithProgress)
            var uploaded = 0L
            inSt.use {
                var read: Int = inSt.read(buffer)
                val handler = Handler(Looper.getMainLooper())
                while (read != -1) {
                    progressCallback?.let {
                        uploaded += read
                        val progress = (uploaded.toDouble() / fileLength.toDouble()).toFloat()
                        handler.post { it(progress) }

                        sink.write(buffer, 0, read)
                    }
                    read = inSt.read(buffer)
                }
            }
        }
    }
}

here is the usage of above function with Flow

private val key = "file"
private val multiPart = "multipart/form-data".toMediaType()

@WorkerThread
fun uploadFile(
    path: String,
    onStart: () -> Unit,
    onComplete: () -> Unit,
    onProgress: (progress: Float) -> Unit,
    onError: (String?) -> Unit
) = flow {
    val file = File(path)
    val requestFile = file.asRequestBodyWithProgress(multiPart, onProgress)
    val requestBody = MultipartBody.Part.createFormData(key, file.name, requestFile)
    //val requestBody = MultipartBody.Builder().addFormDataPart(key, file.name, requestFile).build()

    val response = uploadClient.uploadFile(requestBody)
    response.suspendOnSuccess {
        data.whatIfNotNull {
            emit(it)
        }
    }.onError {
        /** maps the [ApiResponse.Failure.Error] to the [ErrorResponse] using the mapper. */
        map(ErrorResponseMapper) { onError("[Code: $code]: $message") }
    }.onException { onError(message) }
}.onStart { onStart() }.onCompletion { onComplete() }.flowOn(Dispatchers.IO)

UPDATE -> It is very hard to get real path of the file since API 30. So we can used InputStream as below mentioned.

@WorkerThread
fun uploadFile(
    path: String,
    onStart: () -> Unit,
    onComplete: (String?) -> Unit,
    onProgress: (progress: Float) -> Unit,
    onError: (String?) -> Unit
) = flow {
    openStream(path).whatIfNotNull { inputStream ->
        val requestFile = inputStream.asRequestBodyWithProgress(MultipartBody.FORM, onProgress)
        val requestBody = MultipartBody.Part.createFormData(key, "file", requestFile)
        //val requestBody = MultipartBody.Builder().addFormDataPart(key, file.name, requestFile).build()

        uploadClient.uploadFile(requestBody).suspendOnSuccess {
            data.whatIfNotNull {
                link = it.link
                emit(it)
            }
        }.onError {
            /** maps the [ApiResponse.Failure.Error] to the [ErrorResponse] using the mapper. */
            map(ErrorResponseMapper) { onError("[Code: $code]: $message") }
        }.onException { onError(message) }
    }
}.onStart { onStart() }.onCompletion { onComplete(link) }.flowOn(Dispatchers.IO)

private fun openStream(path: String): InputStream? {
    return context.contentResolver.openInputStream(Uri.parse(path))
}


/** Returns a new request body that transmits the content of this. */
fun InputStream.asRequestBodyWithProgress(
    contentType: MediaType? = null,
    progressCallback: ((progress: Float) -> Unit)?
): RequestBody {
    return object : RequestBody() {
        override fun contentType() = contentType

        override fun contentLength() = try {
            available().toLong()
        } catch (e: IOException){
            Timber.e(e)
            0
        }

        override fun writeTo(sink: BufferedSink) {
            val fileLength = contentLength()
            val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
            val inputStream = this@asRequestBodyWithProgress
            var uploaded = 0L
            inputStream.use {
                var read: Int = inputStream.read(buffer)
                val handler = Handler(Looper.getMainLooper())
                while (read != -1) {
                    progressCallback?.let {
                        uploaded += read
                        val progress = (uploaded.toDouble() / fileLength.toDouble()).toFloat()
                        handler.post { it(progress) }

                        sink.write(buffer, 0, read)
                    }
                    read = inputStream.read(buffer)
                }
            }
        }
    }
}
Dilanka Laksiri
  • 408
  • 3
  • 12
1

this answer use for MultipartBody and uploading multiple files. my server-side codes are mvc development. first, you need ApiService class like this:

public interface ApiService {

@POST("Home/UploadVideos")
Call<ResponseBody> postMeme(@Body RequestBody files);
}

and you need Apiclient like this:

public class ApiClient {
public static final String API_BASE_URL = "http://192.168.43.243/Web/";

private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();

private static Retrofit.Builder builder = new Retrofit.Builder().baseUrl(API_BASE_URL).addConverterFactory(GsonConverterFactory.create());

public static ApiService createService(Class<ApiService> serviceClass)
{
    Retrofit retrofit = builder.client(httpClient.build()).build();
    return retrofit.create(serviceClass);
}
}

after that you need ReqestBody class like this:

public class CountingFileRequestBody extends RequestBody {
private static final String TAG = "CountingFileRequestBody";

private final ProgressListener listener;
private final String key;
private final MultipartBody multipartBody;
protected CountingSink mCountingSink;

public CountingFileRequestBody(MultipartBody multipartBody,
                               String key,
                               ProgressListener listener) {
    this.multipartBody = multipartBody;
    this.listener = listener;
    this.key = key;
}

@Override
public long contentLength() throws IOException {
    return multipartBody.contentLength();
}

@Override
public MediaType contentType() {
    return multipartBody.contentType();
}

@Override
public void writeTo(BufferedSink sink) throws IOException {
    mCountingSink = new CountingSink(sink);
    BufferedSink bufferedSink = Okio.buffer(mCountingSink);
    multipartBody.writeTo(bufferedSink);
    bufferedSink.flush();
}

public interface ProgressListener {
    void transferred(String key, int num);
}

protected final class CountingSink extends ForwardingSink {
    private long bytesWritten = 0;

    public CountingSink(Sink delegate) {
        super(delegate);
    }

    @Override
    public void write(Buffer source, long byteCount) throws IOException {
        bytesWritten += byteCount;
        listener.transferred(key, (int) (100F * bytesWritten / contentLength()));
        super.write(source, byteCount);
        delegate().flush(); // I have added this line to manually flush the sink
    }
}

}

and finally, you need this code:

ApiService service = ApiClient.createService(ApiService.class);

        MultipartBody.Builder builder = new MultipartBody.Builder();
        builder.setType(MultipartBody.FORM);
        builder.addFormDataPart("files",file1.getName(), RequestBody.create(MediaType.parse("video/*"), file1));
        builder.addFormDataPart("files",file3.getName(), RequestBody.create(MediaType.parse("video/*"), file3));

        MultipartBody requestBody = builder.build();

        CountingFileRequestBody requestBody1 = new CountingFileRequestBody(requestBody, "files", new CountingFileRequestBody.ProgressListener() {
            @Override
            public void transferred(String key, int num) {
                Log.d("FinishAdapter","Perecentae is :"+num);
                //update progressbar here
                dialog.updateProgress(num);
                if (num == 100){
                    dialog.dismiss();
                }

            }
        });

        Call<ResponseBody> call = service.postMeme(requestBody1);
        call.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
               // Toast.makeText(getBaseContext(),"All fine",Toast.LENGTH_SHORT).show();
                Log.d("FinishAdapter","every thing is ok............!");
                Log.d("FinishAdapter",response.toString());
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                //Toast.makeText(getBaseContext(),t.getMessage(),Toast.LENGTH_SHORT).show();
                Log.d("FinishAdapter","every thing is failed............!");
            }
        });

hope it helps.

saeid aslami
  • 121
  • 1
  • 4
1

I realize this question was answered years ago, but I thought I'd update it for Kotlin:

Create a class that extends RequestBody. Be sure to populate the ContentType enum class to use whichever content types you need to support.

class RequestBodyWithProgress(
    private val file: File,
    private val contentType: ContentType,
    private val progressCallback:((progress: Float)->Unit)?
) : RequestBody() {

    override fun contentType(): MediaType? = MediaType.parse(contentType.description)

    override fun contentLength(): Long = file.length()

    override fun writeTo(sink: BufferedSink) {
        val fileLength = contentLength()
        val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
        val inSt = FileInputStream(file)
        var uploaded = 0L
        inSt.use {
            var read: Int = inSt.read(buffer)
            val handler = Handler(Looper.getMainLooper())
            while (read != -1) {
                progressCallback?.let {
                    uploaded += read
                    val progress = (uploaded.toDouble() / fileLength.toDouble()).toFloat()
                    handler.post { it(progress) }

                    sink.write(buffer, 0, read)
                }
                read = inSt.read(buffer)
            }
        }
    }

    enum class ContentType(val description: String) {
        PNG_IMAGE("image/png"),
        JPG_IMAGE("image/jpg"),
        IMAGE("image/*")
    }
}

Upload the file using Retrofit:

fun uploadFile(fileUri: Uri, progressCallback:((progress: Float)->Unit)?) {
    val file = File(fileUri.path)
    if (!file.exists()) throw FileNotFoundException(fileUri.path)

    // create RequestBody instance from file
    val requestFile = RequestBodyWithProgress(file, RequestBodyWithProgress.ContentType.PNG_IMAGE, progressCallback)

    // MultipartBody.Part is used to send also the actual file name
    val body = MultipartBody.Part.createFormData("image_file", file.name, requestFile)

    publicApiService().uploadFile(body).enqueue(object : Callback<MyResponseObj> {
        override fun onFailure(call: Call<MyResponseObj>, t: Throwable) {

        }

        override fun onResponse(call: Call<MyResponseObj>, response: Response<MyResponseObj>) {

        }
    })

}
Chris
  • 1,180
  • 1
  • 9
  • 17
  • Thanks for the code example but when I try to use it return 3 times from 0.0 -> 1.0 ? Do you got that problem? Thanks. – Khang Tran Jun 13 '21 at 09:44
1

Here is an extension function heavily inspired from this thread and it is working really well.

fun File.toRequestBody(progressCallback: ((progress: Int) -> Unit)?): RequestBody {
    return object : RequestBody() {

        private var currentProgress = 0
        private var uploaded = 0L

        override fun contentType(): MediaType? {
            val fileType = name.substringAfterLast('.', "")
            return fileType.toMediaTypeOrNull()
        }

        @Throws(IOException::class)
        override fun writeTo(sink: BufferedSink) {
            source().use { source ->
                do {
                    val read = source.read(sink.buffer, 2048)
                    if (read == -1L) return // exit at EOF
                    sink.flush()
                    uploaded += read

                    /**
                     * The value of newProgress is going to be in between 0.0 - 2.0
                     */
                    var newProgress = ((uploaded.toDouble() / length().toDouble()))

                    /**
                     * To map it between 0.0 - 100.0
                     * Need to multiply it with 50
                     * (OutputMaxRange/InputMaxRange)
                     * 100 / 2 = 50
                     */
                    newProgress = (50 * newProgress)

                    if (newProgress.toInt() != currentProgress) {
                        progressCallback?.invoke(newProgress.toInt())
                    }
                    currentProgress = newProgress.toInt()
                } while (true)
            }
        }
    }
}
OhhhThatVarun
  • 3,981
  • 2
  • 26
  • 49
  • You should multiply by 100 to get progress between 1 and 100 (percent) since you are dividing uploaded part by total then to get progress you divide by 100 not 50 – Amr Apr 05 '22 at 12:18
  • This solution works great, though I had to multiply by 100 to get the progress – Victor Okech Mar 22 '23 at 10:03
0

I tried to use above code but i found UI was getting stuck so i tried this code this is working for me or may try using this code

Vivek Barai
  • 1,338
  • 13
  • 26
0

To avoid twice running issue. We can set flag as zero initially and set flag as one after the first calling the progress dialog.

 @Override
    public void writeTo(BufferedSink sink) throws IOException {

        Source source = null;
        try {
            source = Okio.source(mFile);
            total = 0;
            long read;

            Handler handler = new Handler(Looper.getMainLooper());

            while ((read = source.read(sink.buffer(), DEFAULT_BUFFER_SIZE)) != -1) {

                total += read;
                sink.flush();

                // flag for avoiding first progress bar .
                if (flag != 0) {
                    handler.post(() -> mListener.onProgressUpdate((int) (100 * total / mFile.length())));

                }
            }

            flag = 1;

        } finally {
            Util.closeQuietly(source);
        }
    }
Sagar Zala
  • 4,854
  • 9
  • 34
  • 62
saneesh
  • 137
  • 11
0

Extension for creating a Part. The callback will be called during invoking service

fun File.toPart(type: String = "image/*", callback: (progress: Int)->Unit) = MultipartBody.Part.createFormData(name, name, object : RequestBody() {
    val contentType = MediaType.parse(type)
    val length = this@toPart.length()
    var uploaded = 0L
    override fun contentType(): MediaType? {
        return contentType
    }

    override fun contentLength(): Long = length

    @Throws(IOException::class)
    override fun writeTo(sink: BufferedSink) {
        var source: Source? = null
        try {
            source = Okio.source(this@toPart)

            do {
                val read = source.read(sink.buffer(), 2048)
                if(read == -1L) return // exit at EOF
                sink.flush()
                uploaded += read
                callback((uploaded.toDouble()/length.toDouble()*100).toInt())
            } while(true)
            //sink.writeAll(source!!)
        } finally {
            Util.closeQuietly(source)
        }
    }
})
A.Y.
  • 389
  • 3
  • 5
0

I appericiate @Yuriy Kolbasinskiy given answer but it gives error for me "expected 3037038 bytes but received 3039232" after I Change some on WriteTo() function. The Answer is in Kotlin which given below :-

override fun writeTo(sink: BufferedSink) {
    var uploaded:Long = 0
    var source: Source? = null
    try {
        source = Okio.source(file)
        val handler = Handler(Looper.getMainLooper())

        do {
            val read = source.read(sink.buffer(), 2048)
            while (read == -1L) return
            uploaded += read

            handler.post(ProgressUpdater(uploaded, file.length()))
            sink.flush()
        } while(true)
    } finally {
        Util.closeQuietly(source)
    }
}
Ali Heikal
  • 3,790
  • 3
  • 18
  • 24
0
@Override
public void writeTo(BufferedSink sink) throws IOException {
    long fileLength = mFile.length();
    byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
    FileInputStream in = new FileInputStream(mFile);
    long uploaded = 0;
    try {
        int read;
        Handler handler = new Handler(Looper.getMainLooper());
        int num = 0;
        while ((read = in.read(buffer)) != -1) {

            int progress = (int) (100 * uploaded / fileLength);
            if (progress > num + 1) {
                mListener.onProgressUpdate((int) (100 * uploaded / fileLength));
                num = progress;
            }
            uploaded += read;
            if (uploaded == fileLength) {
                mListener.onFinish();
            }
            sink.write(buffer, 0, read);
        }
        Log.e("Progress", "erwer");
    } finally {
        in.close();
    }
}

This code guarantees onFinish call.

Rishabh Dhiman
  • 159
  • 2
  • 9
-1

As far as I can see in this post, no updates regarding the image upload progress response has been made and you still have to override the writeTo method as shown in this SO answer by making a ProgressListener interface and using a sub-class of TypedFile to override the writeTo method.

So, there isn't any built-in way to show progress when using retrofit 2 library.

Community
  • 1
  • 1
Shubham A.
  • 2,446
  • 4
  • 36
  • 68
-4

You can use FileUploader that usage Retrofit Library for connecting to API. To upload the file the code skeleton is as follow:

FileUploader fileUploader = new FileUploader();
fileUploader.uploadFiles("/", "file", filesToUpload, new FileUploader.FileUploaderCallback() {
    @Override
    public void onError() {
        // Hide progressbar
    }

    @Override
    public void onFinish(String[] responses) {
        // Hide progressbar

        for(int i=0; i< responses.length; i++){
            String str = responses[i];
            Log.e("RESPONSE "+i, responses[i]);
        }
    }

    @Override
    public void onProgressUpdate(int currentpercent, int totalpercent, int filenumber) {
        // Update Progressbar
        Log.e("Progress Status", currentpercent+" "+totalpercent+" "+filenumber);
    }
});

Full steps are available at Medium:

Retrofit multiple file upload with progress in Android