This is the second tutorial in the Image Uploading with Retrofit series. In the first tutorial, we’d set up our NodeJS server on the localhost. Please refer to this tutorial before proceeding ahead to setup NodeJS server. In this tutorial, we’ll be implementing Image Uploading while showing the upload progress in our android application.
Retrofit MultiPart Image Upload Progress
We hope that you’ve successfully set up the Node JS server in the previous tutorial. In order to know the upload progress, we’ll use OkHttp.
OkHttp is handy in intercepting request and response calls. It has many recipes available here: OkHttp Recipes
We’ll be adapting one of the Recipes (Progress) in order to handle and display the Upload Progress.
The code for the ProgressRequestBody.java is given below:
package com.journaldev.androiduploadimageretrofitnodejs;
import android.os.Handler;
import android.os.Looper;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okio.BufferedSink;
public class ProgressRequestBody extends RequestBody {
private File mFile;
private UploadCallbacks mListener;
private static final int DEFAULT_BUFFER_SIZE = 2048;
public interface UploadCallbacks {
void onProgressUpdate(int percentage);
void onError();
void onFinish();
void uploadStart();
}
public ProgressRequestBody(final File file, final UploadCallbacks listener) {
mFile = file;
mListener = listener;
mListener.uploadStart();
}
@Override
public MediaType contentType() {
// i want to upload only images
return MediaType.parse("image/*");
}
@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) {
uploaded += read;
sink.write(buffer, 0, read);
handler.post(new ProgressUpdater(uploaded, fileLength));
}
} 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() {
try {
int progress = (int) (100 * mUploaded / mTotal);
if (progress == 100)
mListener.onFinish();
else
mListener.onProgressUpdate(progress);
} catch (ArithmeticException e) {
mListener.onError();
e.printStackTrace();
}
}
}
}
In the above code, we’ve defined UploadCallback
Interface which will be implemented in the MainActivity.java with the methods triggered on different events.
Inside the writeTo
function, we calculate the bytes uploaded. Each time, it invokes a runnable class where we trigger the callback methods by calculating the progress (based on current upload length and file length in bytes).
Now that our OKHttp RequestBody is ready, we’re ready to integrate it into our MainActivity inside the retrofit call.
Project Structure
Code
The code for the activity_main.xml layout is given below:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:app="https://schemas.android.com/apk/res-auto"
xmlns:dpv="https://schemas.android.com/apk/res-auto"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<RelativeLayout
android:id="@+id/content_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Display1" />
<ImageView
android:id="@+id/imageView"
android:layout_width="250dp"
android:layout_height="250dp"
android:layout_centerInParent="true"
android:adjustViewBounds="true"
android:scaleType="centerCrop" />
</RelativeLayout>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:srcCompat="@android:drawable/ic_menu_camera" />
<FrameLayout
android:id="@+id/frameLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal">
<android.support.design.widget.FloatingActionButton
android:id="@+id/fabUpload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:srcCompat="@drawable/ic_file_upload" />
</FrameLayout>
</android.support.design.widget.CoordinatorLayout>
fabUpload
view inside a FrameLayout, since Android Support Design Library does not allow to toggle the visiblity of a FloatingActionButton present in a CoordinatorLayout due to layout anchors.The code for the ApiService is the same as in the previous tutorial:
The code for the MainActivity.java is given below:
package com.journaldev.androiduploadimageretrofitnodejs;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import static android.Manifest.permission.CAMERA;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.view.View.GONE;
public class MainActivity extends AppCompatActivity implements View.OnClickListener, ProgressRequestBody.UploadCallbacks {
ApiService apiService;
Uri picUri;
private ArrayList<String> permissionsToRequest;
private ArrayList<String> permissionsRejected = new ArrayList<>();
private ArrayList<String> permissions = new ArrayList<>();
private final static int ALL_PERMISSIONS_RESULT = 107;
private final static int IMAGE_RESULT = 200;
FloatingActionButton fabCamera, fabUpload;
Bitmap mBitmap;
TextView textView;
byte[] byteArray;
FrameLayout frameLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fabCamera = findViewById(R.id.fab);
fabUpload = findViewById(R.id.fabUpload);
textView = findViewById(R.id.textView);
frameLayout = findViewById(R.id.frameLayout);
fabCamera.setOnClickListener(this);
fabUpload.setOnClickListener(this);
askPermissions();
}
private void askPermissions() {
permissions.add(CAMERA);
permissions.add(WRITE_EXTERNAL_STORAGE);
permissions.add(READ_EXTERNAL_STORAGE);
permissionsToRequest = findUnAskedPermissions(permissions);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (permissionsToRequest.size() > 0)
requestPermissions(permissionsToRequest.toArray(new String[permissionsToRequest.size()]), ALL_PERMISSIONS_RESULT);
}
}
private void initRetrofitClient() {
OkHttpClient client = new OkHttpClient.Builder().build();
//change the ip to yours.
apiService = new Retrofit.Builder().baseUrl("https://172.20.10.3:3000").client(client).build().create(ApiService.class);
}
public Intent getPickImageChooserIntent() {
Uri outputFileUri = getCaptureImageOutputUri();
List<Intent> allIntents = new ArrayList<>();
PackageManager packageManager = getPackageManager();
Intent captureIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
List<ResolveInfo> listCam = packageManager.queryIntentActivities(captureIntent, 0);
for (ResolveInfo res : listCam) {
Intent intent = new Intent(captureIntent);
intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
intent.setPackage(res.activityInfo.packageName);
if (outputFileUri != null) {
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
}
allIntents.add(intent);
}
Intent galleryIntent = new Intent(Intent.ACTION_GET_CONTENT);
galleryIntent.setType("image/*");
List<ResolveInfo> listGallery = packageManager.queryIntentActivities(galleryIntent, 0);
for (ResolveInfo res : listGallery) {
Intent intent = new Intent(galleryIntent);
intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
intent.setPackage(res.activityInfo.packageName);
allIntents.add(intent);
}
Intent mainIntent = allIntents.get(allIntents.size() - 1);
for (Intent intent : allIntents) {
if (intent.getComponent().getClassName().equals("com.android.documentsui.DocumentsActivity")) {
mainIntent = intent;
break;
}
}
allIntents.remove(mainIntent);
Intent chooserIntent = Intent.createChooser(mainIntent, "Select source");
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, allIntents.toArray(new Parcelable[allIntents.size()]));
return chooserIntent;
}
private Uri getCaptureImageOutputUri() {
Uri outputFileUri = null;
File getImage = getExternalFilesDir("");
if (getImage != null) {
outputFileUri = Uri.fromFile(new File(getImage.getPath(), "profile.png"));
}
return outputFileUri;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK) {
ImageView imageView = findViewById(R.id.imageView);
if (requestCode == IMAGE_RESULT) {
String filePath = getImageFilePath(data);
if (filePath != null) {
frameLayout.setVisibility(GONE);
mBitmap = BitmapFactory.decodeFile(filePath);
getByteArrayInBackground();
imageView.setImageBitmap(mBitmap);
}
}
}
}
private void getByteArrayInBackground() {
Thread thread = new Thread() {
@Override
public void run() {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
mBitmap.compress(Bitmap.CompressFormat.PNG, 0, bos);
byteArray = bos.toByteArray();
runOnUiThread(new Runnable() {
@Override
public void run() {
frameLayout.setVisibility(View.VISIBLE);
}
});
}
};
thread.start();
}
private String getImageFromFilePath(Intent data) {
boolean isCamera = data == null || data.getData() == null;
if (isCamera) return getCaptureImageOutputUri().getPath();
else return getPathFromURI(data.getData());
}
public String getImageFilePath(Intent data) {
return getImageFromFilePath(data);
}
private String getPathFromURI(Uri contentUri) {
String[] proj = {MediaStore.Audio.Media.DATA};
Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null);
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA);
cursor.moveToFirst();
return cursor.getString(column_index);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable("pic_uri", picUri);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
// get the file url
picUri = savedInstanceState.getParcelable("pic_uri");
}
private ArrayList<String> findUnAskedPermissions(ArrayList<String> wanted) {
ArrayList<String> result = new ArrayList<String>();
for (String perm : wanted) {
if (!hasPermission(perm)) {
result.add(perm);
}
}
return result;
}
private boolean hasPermission(String permission) {
if (canMakeSmores()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED);
}
}
return true;
}
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(this)
.setMessage(message)
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", null)
.create()
.show();
}
private boolean canMakeSmores() {
return (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1);
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case ALL_PERMISSIONS_RESULT:
for (String perms : permissionsToRequest) {
if (!hasPermission(perms)) {
permissionsRejected.add(perms);
}
}
if (permissionsRejected.size() > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (shouldShowRequestPermissionRationale(permissionsRejected.get(0))) {
showMessageOKCancel("These permissions are mandatory for the application. Please allow access.",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermissions(permissionsRejected.toArray(new String[permissionsRejected.size()]), ALL_PERMISSIONS_RESULT);
}
});
return;
}
}
}
break;
}
}
private void multipartImageUpload() {
initRetrofitClient();
try {
if (byteArray != null) {
File filesDir = getApplicationContext().getFilesDir();
File file = new File(filesDir, "image" + ".png");
FileOutputStream fos = new FileOutputStream(file);
fos.write(byteArray);
fos.flush();
fos.close();
textView.setTextColor(Color.BLUE);
ProgressRequestBody fileBody = new ProgressRequestBody(file, this);
MultipartBody.Part body = MultipartBody.Part.createFormData("upload", file.getName(), fileBody);
RequestBody name = RequestBody.create(MediaType.parse("text/plain"), "upload");
Call<ResponseBody> req = apiService.postImage(body, name);
req.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
Toast.makeText(getApplicationContext(), response.code() + " ", Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
textView.setText("Uploaded Failed!");
textView.setTextColor(Color.RED);
Toast.makeText(getApplicationContext(), "Request failed", Toast.LENGTH_SHORT).show();
t.printStackTrace();
}
});
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.fab:
startActivityForResult(getPickImageChooserIntent(), IMAGE_RESULT);
break;
case R.id.fabUpload:
if (mBitmap != null)
multipartImageUpload();
else {
Toast.makeText(getApplicationContext(), "Bitmap is null. Try again", Toast.LENGTH_SHORT).show();
}
break;
}
}
@Override
public void onProgressUpdate(int percentage) {
textView.setText(percentage + "%");
}
@Override
public void onError() {
textView.setText("Uploaded Failed!");
textView.setTextColor(Color.RED);
}
@Override
public void onFinish() {
textView.setText("Uploaded Successfully");
}
@Override
public void uploadStart() {
textView.setText("0%");
Toast.makeText(getApplicationContext(), "Upload started", Toast.LENGTH_SHORT).show();
}
}
In the above code,
We’ve used Runtime Permissions and Capturing Image From Camera And Gallery Using FileProvider.
Coming to the important differences, we’ve optimized the code such that the byte array that gets created from the Bitmap is done in a background thread in order to prevent freezing of the UI thread.
UploadCallbacks
interface is implemented and the text view updates its value while the image uploads.
The output of the above application in action is given below:
That brings an end to this tutorial. You can download the project from the link below:
I downloaded the code and ran it on my device but whenever I try to upload an image it instantly shows upload failed.
The server is working at that time but image is not getting uploaded.
MultipartBody.Part body = MultipartBody.Part.createFormData(“files”, file.getName(), progressRequestBody);
I am using the above line of code to prepare the request body but the problem is writeTo(BufferedSink sink) method is getting called twice. So the progress is moving from 0 to 100 and again from 0 to 100. Could you please explain this?
It is working for me.
Thank you for uploading sir.
Hii sir
please upload more videos on SWIFT and IOS tutorial it’s helping me a lot.
thanks